@monlite/cron 0.2.0 → 0.2.1
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 +17 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +17 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -45,9 +45,8 @@ function parseCron(expr) {
|
|
|
45
45
|
dowRestricted: parts[4] !== "*"
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
function
|
|
49
|
-
if (!c.
|
|
50
|
-
return false;
|
|
48
|
+
function dayMatches(c, p) {
|
|
49
|
+
if (!c.month.has(p.month)) return false;
|
|
51
50
|
const dom = c.dom.has(p.day);
|
|
52
51
|
const dow = c.dow.has(p.dow);
|
|
53
52
|
return c.domRestricted && c.dowRestricted ? dom || dow : dom && dow;
|
|
@@ -106,8 +105,21 @@ function nextCronRun(expr, from = /* @__PURE__ */ new Date(), opts = {}) {
|
|
|
106
105
|
const d = new Date(from.getTime());
|
|
107
106
|
d.setSeconds(0, 0);
|
|
108
107
|
d.setMinutes(d.getMinutes() + 1);
|
|
109
|
-
for (let i = 0; i < 5 * 366 *
|
|
110
|
-
|
|
108
|
+
for (let i = 0; i < 5 * 366 * 25; i++) {
|
|
109
|
+
const p = tz ? tzParts(d, tz) : localParts(d);
|
|
110
|
+
if (!dayMatches(c, p)) {
|
|
111
|
+
if (tz) {
|
|
112
|
+
const mins = (24 - p.hour) * 60 - p.minute;
|
|
113
|
+
d.setTime(d.getTime() + Math.max(1, mins) * 6e4);
|
|
114
|
+
} else d.setHours(24, 0, 0, 0);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (!c.hour.has(p.hour)) {
|
|
118
|
+
if (tz) d.setTime(d.getTime() + (60 - p.minute) * 6e4);
|
|
119
|
+
else d.setHours(d.getHours() + 1, 0, 0, 0);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (c.minute.has(p.minute)) return d;
|
|
111
123
|
if (tz) d.setTime(d.getTime() + 6e4);
|
|
112
124
|
else d.setMinutes(d.getMinutes() + 1);
|
|
113
125
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["EventEmitter"],"mappings":"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","file":"index.cjs","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport type { Monlite, HeartbeatTask } from \"@monlite/core\";\n\nexport interface CronOptions {\n /** How often the scheduler checks for due jobs (ms). Default 1000. */\n checkInterval?: number;\n}\n\n/** Per-schedule options. */\nexport interface ScheduleOptions {\n /**\n * IANA time zone (e.g. `\"Europe/Istanbul\"`) the cron expression is evaluated\n * in, DST included. Default: the server's local time.\n */\n tz?: string;\n /**\n * Add a random delay of up to this many ms to each firing — spreads a\n * thundering herd of schedules that would otherwise fire at the same instant.\n * Default `0`.\n */\n jitter?: number;\n}\n\nexport type CronHandler = () => void | Promise<void>;\n\nexport interface ParsedCron {\n minute: Set<number>;\n hour: Set<number>;\n dom: Set<number>;\n month: Set<number>;\n dow: Set<number>;\n domRestricted: boolean;\n dowRestricted: boolean;\n}\n\nfunction parseField(field: string, min: number, max: number): Set<number> {\n const out = new Set<number>();\n for (const part of field.split(\",\")) {\n const [rangePart, stepPart] = part.split(\"/\");\n const step = stepPart === undefined ? 1 : parseInt(stepPart, 10);\n let lo: number;\n let hi: number;\n if (rangePart === \"*\") {\n lo = min;\n hi = max;\n } else if (rangePart.includes(\"-\")) {\n const [a, b] = rangePart.split(\"-\");\n lo = parseInt(a, 10);\n hi = parseInt(b, 10);\n } else {\n // `N/step` means \"from N up to max, every step\" (e.g. `5/15` → 5,20,35,50);\n // a bare `N` (no step) means exactly {N}.\n lo = parseInt(rangePart, 10);\n hi = stepPart === undefined ? lo : max;\n }\n if (\n Number.isNaN(lo) ||\n Number.isNaN(hi) ||\n Number.isNaN(step) ||\n step < 1 ||\n lo < min ||\n hi > max ||\n lo > hi\n ) {\n throw new Error(`Invalid cron field \"${field}\" (expected ${min}-${max})`);\n }\n for (let v = lo; v <= hi; v += step) out.add(v);\n }\n return out;\n}\n\n/** Parse a standard 5-field cron expression (`min hour dom month dow`). */\nexport function parseCron(expr: string): ParsedCron {\n const parts = expr.trim().split(/\\s+/);\n if (parts.length !== 5) {\n throw new Error(\n `Cron expression must have 5 fields, got ${parts.length}: \"${expr}\"`,\n );\n }\n return {\n minute: parseField(parts[0], 0, 59),\n hour: parseField(parts[1], 0, 23),\n dom: parseField(parts[2], 1, 31),\n month: parseField(parts[3], 1, 12),\n dow: parseField(parts[4], 0, 6),\n domRestricted: parts[2] !== \"*\",\n dowRestricted: parts[4] !== \"*\",\n };\n}\n\n/** Wall-clock fields `c` matches against — day-of-month / day-of-week handled per POSIX. */\ninterface WallParts {\n minute: number;\n hour: number;\n day: number;\n month: number;\n dow: number;\n}\n\nfunction partsMatch(c: ParsedCron, p: WallParts): boolean {\n if (!c.minute.has(p.minute) || !c.hour.has(p.hour) || !c.month.has(p.month))\n return false;\n const dom = c.dom.has(p.day);\n const dow = c.dow.has(p.dow);\n // POSIX: when both day-of-month and day-of-week are restricted, either matches.\n return c.domRestricted && c.dowRestricted ? dom || dow : dom && dow;\n}\n\nconst localParts = (d: Date): WallParts => ({\n minute: d.getMinutes(),\n hour: d.getHours(),\n day: d.getDate(),\n month: d.getMonth() + 1,\n dow: d.getDay(),\n});\n\nconst DOW: Record<string, number> = {\n Sun: 0,\n Mon: 1,\n Tue: 2,\n Wed: 3,\n Thu: 4,\n Fri: 5,\n Sat: 6,\n};\nconst tzFmtCache = new Map<string, Intl.DateTimeFormat>();\nfunction tzFormatter(tz: string): Intl.DateTimeFormat {\n let f = tzFmtCache.get(tz);\n if (!f) {\n // Throws \"Invalid time zone\" on a bad tz — surfaces a clear error to the caller.\n f = new Intl.DateTimeFormat(\"en-US\", {\n timeZone: tz,\n hour12: false,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n weekday: \"short\",\n });\n tzFmtCache.set(tz, f);\n }\n return f;\n}\n\n/** The wall-clock fields of instant `d` as seen in IANA time zone `tz`. */\nfunction tzParts(d: Date, tz: string): WallParts {\n const map: Record<string, string> = {};\n for (const part of tzFormatter(tz).formatToParts(d))\n map[part.type] = part.value;\n let hour = parseInt(map.hour, 10);\n if (hour === 24) hour = 0; // some engines render midnight as \"24\"\n return {\n minute: parseInt(map.minute, 10),\n hour,\n day: parseInt(map.day, 10),\n month: parseInt(map.month, 10),\n dow: DOW[map.weekday] ?? 0,\n };\n}\n\n/**\n * The next time (strictly after `from`) a cron expression fires. Evaluated in\n * local time by default, or in `opts.tz` (an IANA zone like `\"Europe/Istanbul\"`)\n * — handling that zone's DST transitions.\n */\nexport function nextCronRun(\n expr: string | ParsedCron,\n from: Date = new Date(),\n opts: { tz?: string } = {},\n): Date {\n const c = typeof expr === \"string\" ? parseCron(expr) : expr;\n const tz = opts.tz;\n const d = new Date(from.getTime());\n d.setSeconds(0, 0);\n d.setMinutes(d.getMinutes() + 1);\n // Search up to ~5 years so a leap-day-only schedule (`* * 29 2 *`) still resolves\n // across the 4-year gap instead of throwing. Local iteration uses Date arithmetic\n // (skips non-existent days / handles DST natively); the tz path advances absolute\n // time by a minute and reads the zone's wall clock (DST handled by `Intl`).\n for (let i = 0; i < 5 * 366 * 24 * 60; i++) {\n if (partsMatch(c, tz ? tzParts(d, tz) : localParts(d))) return d;\n if (tz) d.setTime(d.getTime() + 60_000);\n else d.setMinutes(d.getMinutes() + 1);\n }\n throw new Error(`Could not compute next run for cron \"${expr}\"`);\n}\n\nconst ensured = new WeakSet<object>();\nconst nowMs = () => Date.now();\n\n/**\n * A persisted cron scheduler. Schedules survive restarts (next-run is stored),\n * and firing is atomic so multiple processes won't double-run an occurrence.\n * Compose with a queue for durable work: `cron.schedule(n, expr, () => queue.add(...))`.\n * Emits `\"error\"` (err, name) if a handler throws.\n */\nexport class Cron extends EventEmitter {\n private readonly driver: Monlite[\"driver\"];\n private readonly heartbeat: Monlite[\"heartbeat\"];\n private readonly checkInterval: number;\n private readonly handlers = new Map<\n string,\n { c: ParsedCron; fn: CronHandler; tz?: string; jitter?: number }\n >();\n private task: HeartbeatTask | undefined;\n\n constructor(db: Monlite, opts: CronOptions = {}) {\n super();\n this.driver = db.driver;\n this.heartbeat = db.heartbeat;\n this.checkInterval = opts.checkInterval ?? 1000;\n if (!ensured.has(db)) {\n this.driver.exec(\n `CREATE TABLE IF NOT EXISTS _schedules (\n name TEXT PRIMARY KEY, cron TEXT NOT NULL,\n next_run INTEGER NOT NULL, last_run INTEGER\n )`,\n );\n ensured.add(db);\n }\n }\n\n /** Compute the next firing (epoch ms) for a schedule, applying tz + jitter. */\n private computeNext(\n c: ParsedCron,\n from: Date,\n tz?: string,\n jitter?: number,\n ): number {\n const base = nextCronRun(c, from, { tz }).getTime();\n return jitter && jitter > 0\n ? base + Math.floor(Math.random() * jitter)\n : base;\n }\n\n /** Register (or update) a schedule and start the scheduler. */\n schedule(\n name: string,\n expr: string,\n handler: CronHandler,\n opts: ScheduleOptions = {},\n ): void {\n const c = parseCron(expr);\n const { tz, jitter } = opts;\n const existing = this.driver\n .prepare(`SELECT next_run, cron FROM _schedules WHERE name = ?`)\n .get(name) as { next_run: number; cron: string } | undefined;\n // Keep the stored next_run only when the expression is unchanged (so a restart\n // doesn't reset timing); if the expr changed, recompute so the new schedule\n // takes effect immediately instead of waiting out the old next_run.\n const next =\n existing && existing.cron === expr\n ? existing.next_run\n : this.computeNext(c, new Date(), tz, jitter);\n this.driver\n .prepare(\n `INSERT INTO _schedules (name, cron, next_run, last_run) VALUES (?, ?, ?, NULL)\n ON CONFLICT(name) DO UPDATE SET cron = excluded.cron, next_run = excluded.next_run`,\n )\n .run(name, expr, next);\n this.handlers.set(name, { c, fn: handler, tz, jitter });\n // One poll on the database's shared heartbeat (coalesced with the reactor,\n // kv pub/sub and queue) instead of a dedicated interval.\n if (!this.task) {\n this.task = this.heartbeat.every(this.checkInterval, () => this.tick());\n }\n }\n\n /** Remove a schedule. */\n unschedule(name: string): void {\n this.handlers.delete(name);\n this.driver.prepare(`DELETE FROM _schedules WHERE name = ?`).run(name);\n }\n\n /** The next scheduled run (epoch ms) for a registered schedule. */\n next(name: string): number | undefined {\n const row = this.driver\n .prepare(`SELECT next_run FROM _schedules WHERE name = ?`)\n .get(name) as { next_run: number } | undefined;\n return row?.next_run;\n }\n\n /** @internal — exposed for tests; runs one scheduling pass. */\n tick(): void {\n const t = nowMs();\n for (const [name, reg] of this.handlers) {\n const row = this.driver\n .prepare(`SELECT next_run FROM _schedules WHERE name = ?`)\n .get(name) as { next_run: number } | undefined;\n if (!row || row.next_run > t) continue;\n const next = this.computeNext(reg.c, new Date(t), reg.tz, reg.jitter);\n // Atomic claim: only the process that flips next_run gets to fire.\n const claimed =\n this.driver\n .prepare(\n `UPDATE _schedules SET last_run = ?, next_run = ? WHERE name = ? AND next_run <= ?`,\n )\n .run(t, next, name, t).changes > 0;\n if (claimed) {\n Promise.resolve()\n .then(() => reg.fn())\n .catch((err) => this.emit(\"error\", err, name));\n }\n }\n }\n\n /** Stop the scheduler (schedules remain persisted). */\n stop(): void {\n if (this.task) this.task.cancel();\n this.task = undefined;\n }\n}\n\n/** Create a cron scheduler over a monlite database. */\nexport function createCron(db: Monlite, opts?: CronOptions): Cron {\n return new Cron(db, opts);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["EventEmitter"],"mappings":"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","file":"index.cjs","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport type { Monlite, HeartbeatTask } from \"@monlite/core\";\n\nexport interface CronOptions {\n /** How often the scheduler checks for due jobs (ms). Default 1000. */\n checkInterval?: number;\n}\n\n/** Per-schedule options. */\nexport interface ScheduleOptions {\n /**\n * IANA time zone (e.g. `\"Europe/Istanbul\"`) the cron expression is evaluated\n * in, DST included. Default: the server's local time.\n */\n tz?: string;\n /**\n * Add a random delay of up to this many ms to each firing — spreads a\n * thundering herd of schedules that would otherwise fire at the same instant.\n * Default `0`.\n */\n jitter?: number;\n}\n\nexport type CronHandler = () => void | Promise<void>;\n\nexport interface ParsedCron {\n minute: Set<number>;\n hour: Set<number>;\n dom: Set<number>;\n month: Set<number>;\n dow: Set<number>;\n domRestricted: boolean;\n dowRestricted: boolean;\n}\n\nfunction parseField(field: string, min: number, max: number): Set<number> {\n const out = new Set<number>();\n for (const part of field.split(\",\")) {\n const [rangePart, stepPart] = part.split(\"/\");\n const step = stepPart === undefined ? 1 : parseInt(stepPart, 10);\n let lo: number;\n let hi: number;\n if (rangePart === \"*\") {\n lo = min;\n hi = max;\n } else if (rangePart.includes(\"-\")) {\n const [a, b] = rangePart.split(\"-\");\n lo = parseInt(a, 10);\n hi = parseInt(b, 10);\n } else {\n // `N/step` means \"from N up to max, every step\" (e.g. `5/15` → 5,20,35,50);\n // a bare `N` (no step) means exactly {N}.\n lo = parseInt(rangePart, 10);\n hi = stepPart === undefined ? lo : max;\n }\n if (\n Number.isNaN(lo) ||\n Number.isNaN(hi) ||\n Number.isNaN(step) ||\n step < 1 ||\n lo < min ||\n hi > max ||\n lo > hi\n ) {\n throw new Error(`Invalid cron field \"${field}\" (expected ${min}-${max})`);\n }\n for (let v = lo; v <= hi; v += step) out.add(v);\n }\n return out;\n}\n\n/** Parse a standard 5-field cron expression (`min hour dom month dow`). */\nexport function parseCron(expr: string): ParsedCron {\n const parts = expr.trim().split(/\\s+/);\n if (parts.length !== 5) {\n throw new Error(\n `Cron expression must have 5 fields, got ${parts.length}: \"${expr}\"`,\n );\n }\n return {\n minute: parseField(parts[0], 0, 59),\n hour: parseField(parts[1], 0, 23),\n dom: parseField(parts[2], 1, 31),\n month: parseField(parts[3], 1, 12),\n dow: parseField(parts[4], 0, 6),\n domRestricted: parts[2] !== \"*\",\n dowRestricted: parts[4] !== \"*\",\n };\n}\n\n/** Wall-clock fields `c` matches against — day-of-month / day-of-week handled per POSIX. */\ninterface WallParts {\n minute: number;\n hour: number;\n day: number;\n month: number;\n dow: number;\n}\n\n/** True if the date part (month + day-of-month/day-of-week) of `p` can match. */\nfunction dayMatches(c: ParsedCron, p: WallParts): boolean {\n if (!c.month.has(p.month)) return false;\n const dom = c.dom.has(p.day);\n const dow = c.dow.has(p.dow);\n // POSIX: when both day-of-month and day-of-week are restricted, either matches.\n return c.domRestricted && c.dowRestricted ? dom || dow : dom && dow;\n}\n\nconst localParts = (d: Date): WallParts => ({\n minute: d.getMinutes(),\n hour: d.getHours(),\n day: d.getDate(),\n month: d.getMonth() + 1,\n dow: d.getDay(),\n});\n\nconst DOW: Record<string, number> = {\n Sun: 0,\n Mon: 1,\n Tue: 2,\n Wed: 3,\n Thu: 4,\n Fri: 5,\n Sat: 6,\n};\nconst tzFmtCache = new Map<string, Intl.DateTimeFormat>();\nfunction tzFormatter(tz: string): Intl.DateTimeFormat {\n let f = tzFmtCache.get(tz);\n if (!f) {\n // Throws \"Invalid time zone\" on a bad tz — surfaces a clear error to the caller.\n f = new Intl.DateTimeFormat(\"en-US\", {\n timeZone: tz,\n hour12: false,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n weekday: \"short\",\n });\n tzFmtCache.set(tz, f);\n }\n return f;\n}\n\n/** The wall-clock fields of instant `d` as seen in IANA time zone `tz`. */\nfunction tzParts(d: Date, tz: string): WallParts {\n const map: Record<string, string> = {};\n for (const part of tzFormatter(tz).formatToParts(d))\n map[part.type] = part.value;\n let hour = parseInt(map.hour, 10);\n if (hour === 24) hour = 0; // some engines render midnight as \"24\"\n return {\n minute: parseInt(map.minute, 10),\n hour,\n day: parseInt(map.day, 10),\n month: parseInt(map.month, 10),\n dow: DOW[map.weekday] ?? 0,\n };\n}\n\n/**\n * The next time (strictly after `from`) a cron expression fires. Evaluated in\n * local time by default, or in `opts.tz` (an IANA zone like `\"Europe/Istanbul\"`)\n * — handling that zone's DST transitions.\n */\nexport function nextCronRun(\n expr: string | ParsedCron,\n from: Date = new Date(),\n opts: { tz?: string } = {},\n): Date {\n const c = typeof expr === \"string\" ? parseCron(expr) : expr;\n const tz = opts.tz;\n const d = new Date(from.getTime());\n d.setSeconds(0, 0);\n d.setMinutes(d.getMinutes() + 1);\n // Search up to ~5 years so a leap-day-only schedule (`* * 29 2 *`) still resolves\n // across the 4-year gap instead of throwing. We SKIP whole non-matching days and\n // hours instead of scanning minute-by-minute, so even the tz path (which calls\n // Intl per step) stays in the thousands of iterations, not millions — otherwise a\n // single tz/leap-day schedule would freeze the event loop. DST is handled by Date\n // arithmetic (local) / `Intl` (tz); the coarse jumps are self-correcting since the\n // loop re-reads the wall clock each step.\n for (let i = 0; i < 5 * 366 * 25; i++) {\n const p = tz ? tzParts(d, tz) : localParts(d);\n if (!dayMatches(c, p)) {\n // Skip to the next day's 00:00. Local uses Date methods (exact + DST-safe);\n // tz advances absolute time (a DST transition that day can leave the landing\n // off by ≤1h, self-corrected on the next pass).\n if (tz) {\n const mins = (24 - p.hour) * 60 - p.minute;\n d.setTime(d.getTime() + Math.max(1, mins) * 60_000);\n } else d.setHours(24, 0, 0, 0);\n continue;\n }\n if (!c.hour.has(p.hour)) {\n if (tz) d.setTime(d.getTime() + (60 - p.minute) * 60_000);\n else d.setHours(d.getHours() + 1, 0, 0, 0); // next hour\n continue;\n }\n if (c.minute.has(p.minute)) return d;\n if (tz) d.setTime(d.getTime() + 60_000);\n else d.setMinutes(d.getMinutes() + 1);\n }\n throw new Error(`Could not compute next run for cron \"${expr}\"`);\n}\n\nconst ensured = new WeakSet<object>();\nconst nowMs = () => Date.now();\n\n/**\n * A persisted cron scheduler. Schedules survive restarts (next-run is stored),\n * and firing is atomic so multiple processes won't double-run an occurrence.\n * Compose with a queue for durable work: `cron.schedule(n, expr, () => queue.add(...))`.\n * Emits `\"error\"` (err, name) if a handler throws.\n */\nexport class Cron extends EventEmitter {\n private readonly driver: Monlite[\"driver\"];\n private readonly heartbeat: Monlite[\"heartbeat\"];\n private readonly checkInterval: number;\n private readonly handlers = new Map<\n string,\n { c: ParsedCron; fn: CronHandler; tz?: string; jitter?: number }\n >();\n private task: HeartbeatTask | undefined;\n\n constructor(db: Monlite, opts: CronOptions = {}) {\n super();\n this.driver = db.driver;\n this.heartbeat = db.heartbeat;\n this.checkInterval = opts.checkInterval ?? 1000;\n if (!ensured.has(db)) {\n this.driver.exec(\n `CREATE TABLE IF NOT EXISTS _schedules (\n name TEXT PRIMARY KEY, cron TEXT NOT NULL,\n next_run INTEGER NOT NULL, last_run INTEGER\n )`,\n );\n ensured.add(db);\n }\n }\n\n /** Compute the next firing (epoch ms) for a schedule, applying tz + jitter. */\n private computeNext(\n c: ParsedCron,\n from: Date,\n tz?: string,\n jitter?: number,\n ): number {\n const base = nextCronRun(c, from, { tz }).getTime();\n return jitter && jitter > 0\n ? base + Math.floor(Math.random() * jitter)\n : base;\n }\n\n /** Register (or update) a schedule and start the scheduler. */\n schedule(\n name: string,\n expr: string,\n handler: CronHandler,\n opts: ScheduleOptions = {},\n ): void {\n const c = parseCron(expr);\n const { tz, jitter } = opts;\n const existing = this.driver\n .prepare(`SELECT next_run, cron FROM _schedules WHERE name = ?`)\n .get(name) as { next_run: number; cron: string } | undefined;\n // Keep the stored next_run only when the expression is unchanged (so a restart\n // doesn't reset timing); if the expr changed, recompute so the new schedule\n // takes effect immediately instead of waiting out the old next_run.\n const next =\n existing && existing.cron === expr\n ? existing.next_run\n : this.computeNext(c, new Date(), tz, jitter);\n this.driver\n .prepare(\n `INSERT INTO _schedules (name, cron, next_run, last_run) VALUES (?, ?, ?, NULL)\n ON CONFLICT(name) DO UPDATE SET cron = excluded.cron, next_run = excluded.next_run`,\n )\n .run(name, expr, next);\n this.handlers.set(name, { c, fn: handler, tz, jitter });\n // One poll on the database's shared heartbeat (coalesced with the reactor,\n // kv pub/sub and queue) instead of a dedicated interval.\n if (!this.task) {\n this.task = this.heartbeat.every(this.checkInterval, () => this.tick());\n }\n }\n\n /** Remove a schedule. */\n unschedule(name: string): void {\n this.handlers.delete(name);\n this.driver.prepare(`DELETE FROM _schedules WHERE name = ?`).run(name);\n }\n\n /** The next scheduled run (epoch ms) for a registered schedule. */\n next(name: string): number | undefined {\n const row = this.driver\n .prepare(`SELECT next_run FROM _schedules WHERE name = ?`)\n .get(name) as { next_run: number } | undefined;\n return row?.next_run;\n }\n\n /** @internal — exposed for tests; runs one scheduling pass. */\n tick(): void {\n const t = nowMs();\n for (const [name, reg] of this.handlers) {\n const row = this.driver\n .prepare(`SELECT next_run FROM _schedules WHERE name = ?`)\n .get(name) as { next_run: number } | undefined;\n if (!row || row.next_run > t) continue;\n const next = this.computeNext(reg.c, new Date(t), reg.tz, reg.jitter);\n // Atomic claim: only the process that flips next_run gets to fire.\n const claimed =\n this.driver\n .prepare(\n `UPDATE _schedules SET last_run = ?, next_run = ? WHERE name = ? AND next_run <= ?`,\n )\n .run(t, next, name, t).changes > 0;\n if (claimed) {\n Promise.resolve()\n .then(() => reg.fn())\n .catch((err) => this.emit(\"error\", err, name));\n }\n }\n }\n\n /** Stop the scheduler (schedules remain persisted). */\n stop(): void {\n if (this.task) this.task.cancel();\n this.task = undefined;\n }\n}\n\n/** Create a cron scheduler over a monlite database. */\nexport function createCron(db: Monlite, opts?: CronOptions): Cron {\n return new Cron(db, opts);\n}\n"]}
|
package/dist/index.js
CHANGED
|
@@ -43,9 +43,8 @@ function parseCron(expr) {
|
|
|
43
43
|
dowRestricted: parts[4] !== "*"
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
-
function
|
|
47
|
-
if (!c.
|
|
48
|
-
return false;
|
|
46
|
+
function dayMatches(c, p) {
|
|
47
|
+
if (!c.month.has(p.month)) return false;
|
|
49
48
|
const dom = c.dom.has(p.day);
|
|
50
49
|
const dow = c.dow.has(p.dow);
|
|
51
50
|
return c.domRestricted && c.dowRestricted ? dom || dow : dom && dow;
|
|
@@ -104,8 +103,21 @@ function nextCronRun(expr, from = /* @__PURE__ */ new Date(), opts = {}) {
|
|
|
104
103
|
const d = new Date(from.getTime());
|
|
105
104
|
d.setSeconds(0, 0);
|
|
106
105
|
d.setMinutes(d.getMinutes() + 1);
|
|
107
|
-
for (let i = 0; i < 5 * 366 *
|
|
108
|
-
|
|
106
|
+
for (let i = 0; i < 5 * 366 * 25; i++) {
|
|
107
|
+
const p = tz ? tzParts(d, tz) : localParts(d);
|
|
108
|
+
if (!dayMatches(c, p)) {
|
|
109
|
+
if (tz) {
|
|
110
|
+
const mins = (24 - p.hour) * 60 - p.minute;
|
|
111
|
+
d.setTime(d.getTime() + Math.max(1, mins) * 6e4);
|
|
112
|
+
} else d.setHours(24, 0, 0, 0);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!c.hour.has(p.hour)) {
|
|
116
|
+
if (tz) d.setTime(d.getTime() + (60 - p.minute) * 6e4);
|
|
117
|
+
else d.setHours(d.getHours() + 1, 0, 0, 0);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (c.minute.has(p.minute)) return d;
|
|
109
121
|
if (tz) d.setTime(d.getTime() + 6e4);
|
|
110
122
|
else d.setMinutes(d.getMinutes() + 1);
|
|
111
123
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport type { Monlite, HeartbeatTask } from \"@monlite/core\";\n\nexport interface CronOptions {\n /** How often the scheduler checks for due jobs (ms). Default 1000. */\n checkInterval?: number;\n}\n\n/** Per-schedule options. */\nexport interface ScheduleOptions {\n /**\n * IANA time zone (e.g. `\"Europe/Istanbul\"`) the cron expression is evaluated\n * in, DST included. Default: the server's local time.\n */\n tz?: string;\n /**\n * Add a random delay of up to this many ms to each firing — spreads a\n * thundering herd of schedules that would otherwise fire at the same instant.\n * Default `0`.\n */\n jitter?: number;\n}\n\nexport type CronHandler = () => void | Promise<void>;\n\nexport interface ParsedCron {\n minute: Set<number>;\n hour: Set<number>;\n dom: Set<number>;\n month: Set<number>;\n dow: Set<number>;\n domRestricted: boolean;\n dowRestricted: boolean;\n}\n\nfunction parseField(field: string, min: number, max: number): Set<number> {\n const out = new Set<number>();\n for (const part of field.split(\",\")) {\n const [rangePart, stepPart] = part.split(\"/\");\n const step = stepPart === undefined ? 1 : parseInt(stepPart, 10);\n let lo: number;\n let hi: number;\n if (rangePart === \"*\") {\n lo = min;\n hi = max;\n } else if (rangePart.includes(\"-\")) {\n const [a, b] = rangePart.split(\"-\");\n lo = parseInt(a, 10);\n hi = parseInt(b, 10);\n } else {\n // `N/step` means \"from N up to max, every step\" (e.g. `5/15` → 5,20,35,50);\n // a bare `N` (no step) means exactly {N}.\n lo = parseInt(rangePart, 10);\n hi = stepPart === undefined ? lo : max;\n }\n if (\n Number.isNaN(lo) ||\n Number.isNaN(hi) ||\n Number.isNaN(step) ||\n step < 1 ||\n lo < min ||\n hi > max ||\n lo > hi\n ) {\n throw new Error(`Invalid cron field \"${field}\" (expected ${min}-${max})`);\n }\n for (let v = lo; v <= hi; v += step) out.add(v);\n }\n return out;\n}\n\n/** Parse a standard 5-field cron expression (`min hour dom month dow`). */\nexport function parseCron(expr: string): ParsedCron {\n const parts = expr.trim().split(/\\s+/);\n if (parts.length !== 5) {\n throw new Error(\n `Cron expression must have 5 fields, got ${parts.length}: \"${expr}\"`,\n );\n }\n return {\n minute: parseField(parts[0], 0, 59),\n hour: parseField(parts[1], 0, 23),\n dom: parseField(parts[2], 1, 31),\n month: parseField(parts[3], 1, 12),\n dow: parseField(parts[4], 0, 6),\n domRestricted: parts[2] !== \"*\",\n dowRestricted: parts[4] !== \"*\",\n };\n}\n\n/** Wall-clock fields `c` matches against — day-of-month / day-of-week handled per POSIX. */\ninterface WallParts {\n minute: number;\n hour: number;\n day: number;\n month: number;\n dow: number;\n}\n\nfunction partsMatch(c: ParsedCron, p: WallParts): boolean {\n if (!c.minute.has(p.minute) || !c.hour.has(p.hour) || !c.month.has(p.month))\n return false;\n const dom = c.dom.has(p.day);\n const dow = c.dow.has(p.dow);\n // POSIX: when both day-of-month and day-of-week are restricted, either matches.\n return c.domRestricted && c.dowRestricted ? dom || dow : dom && dow;\n}\n\nconst localParts = (d: Date): WallParts => ({\n minute: d.getMinutes(),\n hour: d.getHours(),\n day: d.getDate(),\n month: d.getMonth() + 1,\n dow: d.getDay(),\n});\n\nconst DOW: Record<string, number> = {\n Sun: 0,\n Mon: 1,\n Tue: 2,\n Wed: 3,\n Thu: 4,\n Fri: 5,\n Sat: 6,\n};\nconst tzFmtCache = new Map<string, Intl.DateTimeFormat>();\nfunction tzFormatter(tz: string): Intl.DateTimeFormat {\n let f = tzFmtCache.get(tz);\n if (!f) {\n // Throws \"Invalid time zone\" on a bad tz — surfaces a clear error to the caller.\n f = new Intl.DateTimeFormat(\"en-US\", {\n timeZone: tz,\n hour12: false,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n weekday: \"short\",\n });\n tzFmtCache.set(tz, f);\n }\n return f;\n}\n\n/** The wall-clock fields of instant `d` as seen in IANA time zone `tz`. */\nfunction tzParts(d: Date, tz: string): WallParts {\n const map: Record<string, string> = {};\n for (const part of tzFormatter(tz).formatToParts(d))\n map[part.type] = part.value;\n let hour = parseInt(map.hour, 10);\n if (hour === 24) hour = 0; // some engines render midnight as \"24\"\n return {\n minute: parseInt(map.minute, 10),\n hour,\n day: parseInt(map.day, 10),\n month: parseInt(map.month, 10),\n dow: DOW[map.weekday] ?? 0,\n };\n}\n\n/**\n * The next time (strictly after `from`) a cron expression fires. Evaluated in\n * local time by default, or in `opts.tz` (an IANA zone like `\"Europe/Istanbul\"`)\n * — handling that zone's DST transitions.\n */\nexport function nextCronRun(\n expr: string | ParsedCron,\n from: Date = new Date(),\n opts: { tz?: string } = {},\n): Date {\n const c = typeof expr === \"string\" ? parseCron(expr) : expr;\n const tz = opts.tz;\n const d = new Date(from.getTime());\n d.setSeconds(0, 0);\n d.setMinutes(d.getMinutes() + 1);\n // Search up to ~5 years so a leap-day-only schedule (`* * 29 2 *`) still resolves\n // across the 4-year gap instead of throwing. Local iteration uses Date arithmetic\n // (skips non-existent days / handles DST natively); the tz path advances absolute\n // time by a minute and reads the zone's wall clock (DST handled by `Intl`).\n for (let i = 0; i < 5 * 366 * 24 * 60; i++) {\n if (partsMatch(c, tz ? tzParts(d, tz) : localParts(d))) return d;\n if (tz) d.setTime(d.getTime() + 60_000);\n else d.setMinutes(d.getMinutes() + 1);\n }\n throw new Error(`Could not compute next run for cron \"${expr}\"`);\n}\n\nconst ensured = new WeakSet<object>();\nconst nowMs = () => Date.now();\n\n/**\n * A persisted cron scheduler. Schedules survive restarts (next-run is stored),\n * and firing is atomic so multiple processes won't double-run an occurrence.\n * Compose with a queue for durable work: `cron.schedule(n, expr, () => queue.add(...))`.\n * Emits `\"error\"` (err, name) if a handler throws.\n */\nexport class Cron extends EventEmitter {\n private readonly driver: Monlite[\"driver\"];\n private readonly heartbeat: Monlite[\"heartbeat\"];\n private readonly checkInterval: number;\n private readonly handlers = new Map<\n string,\n { c: ParsedCron; fn: CronHandler; tz?: string; jitter?: number }\n >();\n private task: HeartbeatTask | undefined;\n\n constructor(db: Monlite, opts: CronOptions = {}) {\n super();\n this.driver = db.driver;\n this.heartbeat = db.heartbeat;\n this.checkInterval = opts.checkInterval ?? 1000;\n if (!ensured.has(db)) {\n this.driver.exec(\n `CREATE TABLE IF NOT EXISTS _schedules (\n name TEXT PRIMARY KEY, cron TEXT NOT NULL,\n next_run INTEGER NOT NULL, last_run INTEGER\n )`,\n );\n ensured.add(db);\n }\n }\n\n /** Compute the next firing (epoch ms) for a schedule, applying tz + jitter. */\n private computeNext(\n c: ParsedCron,\n from: Date,\n tz?: string,\n jitter?: number,\n ): number {\n const base = nextCronRun(c, from, { tz }).getTime();\n return jitter && jitter > 0\n ? base + Math.floor(Math.random() * jitter)\n : base;\n }\n\n /** Register (or update) a schedule and start the scheduler. */\n schedule(\n name: string,\n expr: string,\n handler: CronHandler,\n opts: ScheduleOptions = {},\n ): void {\n const c = parseCron(expr);\n const { tz, jitter } = opts;\n const existing = this.driver\n .prepare(`SELECT next_run, cron FROM _schedules WHERE name = ?`)\n .get(name) as { next_run: number; cron: string } | undefined;\n // Keep the stored next_run only when the expression is unchanged (so a restart\n // doesn't reset timing); if the expr changed, recompute so the new schedule\n // takes effect immediately instead of waiting out the old next_run.\n const next =\n existing && existing.cron === expr\n ? existing.next_run\n : this.computeNext(c, new Date(), tz, jitter);\n this.driver\n .prepare(\n `INSERT INTO _schedules (name, cron, next_run, last_run) VALUES (?, ?, ?, NULL)\n ON CONFLICT(name) DO UPDATE SET cron = excluded.cron, next_run = excluded.next_run`,\n )\n .run(name, expr, next);\n this.handlers.set(name, { c, fn: handler, tz, jitter });\n // One poll on the database's shared heartbeat (coalesced with the reactor,\n // kv pub/sub and queue) instead of a dedicated interval.\n if (!this.task) {\n this.task = this.heartbeat.every(this.checkInterval, () => this.tick());\n }\n }\n\n /** Remove a schedule. */\n unschedule(name: string): void {\n this.handlers.delete(name);\n this.driver.prepare(`DELETE FROM _schedules WHERE name = ?`).run(name);\n }\n\n /** The next scheduled run (epoch ms) for a registered schedule. */\n next(name: string): number | undefined {\n const row = this.driver\n .prepare(`SELECT next_run FROM _schedules WHERE name = ?`)\n .get(name) as { next_run: number } | undefined;\n return row?.next_run;\n }\n\n /** @internal — exposed for tests; runs one scheduling pass. */\n tick(): void {\n const t = nowMs();\n for (const [name, reg] of this.handlers) {\n const row = this.driver\n .prepare(`SELECT next_run FROM _schedules WHERE name = ?`)\n .get(name) as { next_run: number } | undefined;\n if (!row || row.next_run > t) continue;\n const next = this.computeNext(reg.c, new Date(t), reg.tz, reg.jitter);\n // Atomic claim: only the process that flips next_run gets to fire.\n const claimed =\n this.driver\n .prepare(\n `UPDATE _schedules SET last_run = ?, next_run = ? WHERE name = ? AND next_run <= ?`,\n )\n .run(t, next, name, t).changes > 0;\n if (claimed) {\n Promise.resolve()\n .then(() => reg.fn())\n .catch((err) => this.emit(\"error\", err, name));\n }\n }\n }\n\n /** Stop the scheduler (schedules remain persisted). */\n stop(): void {\n if (this.task) this.task.cancel();\n this.task = undefined;\n }\n}\n\n/** Create a cron scheduler over a monlite database. */\nexport function createCron(db: Monlite, opts?: CronOptions): Cron {\n return new Cron(db, opts);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport type { Monlite, HeartbeatTask } from \"@monlite/core\";\n\nexport interface CronOptions {\n /** How often the scheduler checks for due jobs (ms). Default 1000. */\n checkInterval?: number;\n}\n\n/** Per-schedule options. */\nexport interface ScheduleOptions {\n /**\n * IANA time zone (e.g. `\"Europe/Istanbul\"`) the cron expression is evaluated\n * in, DST included. Default: the server's local time.\n */\n tz?: string;\n /**\n * Add a random delay of up to this many ms to each firing — spreads a\n * thundering herd of schedules that would otherwise fire at the same instant.\n * Default `0`.\n */\n jitter?: number;\n}\n\nexport type CronHandler = () => void | Promise<void>;\n\nexport interface ParsedCron {\n minute: Set<number>;\n hour: Set<number>;\n dom: Set<number>;\n month: Set<number>;\n dow: Set<number>;\n domRestricted: boolean;\n dowRestricted: boolean;\n}\n\nfunction parseField(field: string, min: number, max: number): Set<number> {\n const out = new Set<number>();\n for (const part of field.split(\",\")) {\n const [rangePart, stepPart] = part.split(\"/\");\n const step = stepPart === undefined ? 1 : parseInt(stepPart, 10);\n let lo: number;\n let hi: number;\n if (rangePart === \"*\") {\n lo = min;\n hi = max;\n } else if (rangePart.includes(\"-\")) {\n const [a, b] = rangePart.split(\"-\");\n lo = parseInt(a, 10);\n hi = parseInt(b, 10);\n } else {\n // `N/step` means \"from N up to max, every step\" (e.g. `5/15` → 5,20,35,50);\n // a bare `N` (no step) means exactly {N}.\n lo = parseInt(rangePart, 10);\n hi = stepPart === undefined ? lo : max;\n }\n if (\n Number.isNaN(lo) ||\n Number.isNaN(hi) ||\n Number.isNaN(step) ||\n step < 1 ||\n lo < min ||\n hi > max ||\n lo > hi\n ) {\n throw new Error(`Invalid cron field \"${field}\" (expected ${min}-${max})`);\n }\n for (let v = lo; v <= hi; v += step) out.add(v);\n }\n return out;\n}\n\n/** Parse a standard 5-field cron expression (`min hour dom month dow`). */\nexport function parseCron(expr: string): ParsedCron {\n const parts = expr.trim().split(/\\s+/);\n if (parts.length !== 5) {\n throw new Error(\n `Cron expression must have 5 fields, got ${parts.length}: \"${expr}\"`,\n );\n }\n return {\n minute: parseField(parts[0], 0, 59),\n hour: parseField(parts[1], 0, 23),\n dom: parseField(parts[2], 1, 31),\n month: parseField(parts[3], 1, 12),\n dow: parseField(parts[4], 0, 6),\n domRestricted: parts[2] !== \"*\",\n dowRestricted: parts[4] !== \"*\",\n };\n}\n\n/** Wall-clock fields `c` matches against — day-of-month / day-of-week handled per POSIX. */\ninterface WallParts {\n minute: number;\n hour: number;\n day: number;\n month: number;\n dow: number;\n}\n\n/** True if the date part (month + day-of-month/day-of-week) of `p` can match. */\nfunction dayMatches(c: ParsedCron, p: WallParts): boolean {\n if (!c.month.has(p.month)) return false;\n const dom = c.dom.has(p.day);\n const dow = c.dow.has(p.dow);\n // POSIX: when both day-of-month and day-of-week are restricted, either matches.\n return c.domRestricted && c.dowRestricted ? dom || dow : dom && dow;\n}\n\nconst localParts = (d: Date): WallParts => ({\n minute: d.getMinutes(),\n hour: d.getHours(),\n day: d.getDate(),\n month: d.getMonth() + 1,\n dow: d.getDay(),\n});\n\nconst DOW: Record<string, number> = {\n Sun: 0,\n Mon: 1,\n Tue: 2,\n Wed: 3,\n Thu: 4,\n Fri: 5,\n Sat: 6,\n};\nconst tzFmtCache = new Map<string, Intl.DateTimeFormat>();\nfunction tzFormatter(tz: string): Intl.DateTimeFormat {\n let f = tzFmtCache.get(tz);\n if (!f) {\n // Throws \"Invalid time zone\" on a bad tz — surfaces a clear error to the caller.\n f = new Intl.DateTimeFormat(\"en-US\", {\n timeZone: tz,\n hour12: false,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n weekday: \"short\",\n });\n tzFmtCache.set(tz, f);\n }\n return f;\n}\n\n/** The wall-clock fields of instant `d` as seen in IANA time zone `tz`. */\nfunction tzParts(d: Date, tz: string): WallParts {\n const map: Record<string, string> = {};\n for (const part of tzFormatter(tz).formatToParts(d))\n map[part.type] = part.value;\n let hour = parseInt(map.hour, 10);\n if (hour === 24) hour = 0; // some engines render midnight as \"24\"\n return {\n minute: parseInt(map.minute, 10),\n hour,\n day: parseInt(map.day, 10),\n month: parseInt(map.month, 10),\n dow: DOW[map.weekday] ?? 0,\n };\n}\n\n/**\n * The next time (strictly after `from`) a cron expression fires. Evaluated in\n * local time by default, or in `opts.tz` (an IANA zone like `\"Europe/Istanbul\"`)\n * — handling that zone's DST transitions.\n */\nexport function nextCronRun(\n expr: string | ParsedCron,\n from: Date = new Date(),\n opts: { tz?: string } = {},\n): Date {\n const c = typeof expr === \"string\" ? parseCron(expr) : expr;\n const tz = opts.tz;\n const d = new Date(from.getTime());\n d.setSeconds(0, 0);\n d.setMinutes(d.getMinutes() + 1);\n // Search up to ~5 years so a leap-day-only schedule (`* * 29 2 *`) still resolves\n // across the 4-year gap instead of throwing. We SKIP whole non-matching days and\n // hours instead of scanning minute-by-minute, so even the tz path (which calls\n // Intl per step) stays in the thousands of iterations, not millions — otherwise a\n // single tz/leap-day schedule would freeze the event loop. DST is handled by Date\n // arithmetic (local) / `Intl` (tz); the coarse jumps are self-correcting since the\n // loop re-reads the wall clock each step.\n for (let i = 0; i < 5 * 366 * 25; i++) {\n const p = tz ? tzParts(d, tz) : localParts(d);\n if (!dayMatches(c, p)) {\n // Skip to the next day's 00:00. Local uses Date methods (exact + DST-safe);\n // tz advances absolute time (a DST transition that day can leave the landing\n // off by ≤1h, self-corrected on the next pass).\n if (tz) {\n const mins = (24 - p.hour) * 60 - p.minute;\n d.setTime(d.getTime() + Math.max(1, mins) * 60_000);\n } else d.setHours(24, 0, 0, 0);\n continue;\n }\n if (!c.hour.has(p.hour)) {\n if (tz) d.setTime(d.getTime() + (60 - p.minute) * 60_000);\n else d.setHours(d.getHours() + 1, 0, 0, 0); // next hour\n continue;\n }\n if (c.minute.has(p.minute)) return d;\n if (tz) d.setTime(d.getTime() + 60_000);\n else d.setMinutes(d.getMinutes() + 1);\n }\n throw new Error(`Could not compute next run for cron \"${expr}\"`);\n}\n\nconst ensured = new WeakSet<object>();\nconst nowMs = () => Date.now();\n\n/**\n * A persisted cron scheduler. Schedules survive restarts (next-run is stored),\n * and firing is atomic so multiple processes won't double-run an occurrence.\n * Compose with a queue for durable work: `cron.schedule(n, expr, () => queue.add(...))`.\n * Emits `\"error\"` (err, name) if a handler throws.\n */\nexport class Cron extends EventEmitter {\n private readonly driver: Monlite[\"driver\"];\n private readonly heartbeat: Monlite[\"heartbeat\"];\n private readonly checkInterval: number;\n private readonly handlers = new Map<\n string,\n { c: ParsedCron; fn: CronHandler; tz?: string; jitter?: number }\n >();\n private task: HeartbeatTask | undefined;\n\n constructor(db: Monlite, opts: CronOptions = {}) {\n super();\n this.driver = db.driver;\n this.heartbeat = db.heartbeat;\n this.checkInterval = opts.checkInterval ?? 1000;\n if (!ensured.has(db)) {\n this.driver.exec(\n `CREATE TABLE IF NOT EXISTS _schedules (\n name TEXT PRIMARY KEY, cron TEXT NOT NULL,\n next_run INTEGER NOT NULL, last_run INTEGER\n )`,\n );\n ensured.add(db);\n }\n }\n\n /** Compute the next firing (epoch ms) for a schedule, applying tz + jitter. */\n private computeNext(\n c: ParsedCron,\n from: Date,\n tz?: string,\n jitter?: number,\n ): number {\n const base = nextCronRun(c, from, { tz }).getTime();\n return jitter && jitter > 0\n ? base + Math.floor(Math.random() * jitter)\n : base;\n }\n\n /** Register (or update) a schedule and start the scheduler. */\n schedule(\n name: string,\n expr: string,\n handler: CronHandler,\n opts: ScheduleOptions = {},\n ): void {\n const c = parseCron(expr);\n const { tz, jitter } = opts;\n const existing = this.driver\n .prepare(`SELECT next_run, cron FROM _schedules WHERE name = ?`)\n .get(name) as { next_run: number; cron: string } | undefined;\n // Keep the stored next_run only when the expression is unchanged (so a restart\n // doesn't reset timing); if the expr changed, recompute so the new schedule\n // takes effect immediately instead of waiting out the old next_run.\n const next =\n existing && existing.cron === expr\n ? existing.next_run\n : this.computeNext(c, new Date(), tz, jitter);\n this.driver\n .prepare(\n `INSERT INTO _schedules (name, cron, next_run, last_run) VALUES (?, ?, ?, NULL)\n ON CONFLICT(name) DO UPDATE SET cron = excluded.cron, next_run = excluded.next_run`,\n )\n .run(name, expr, next);\n this.handlers.set(name, { c, fn: handler, tz, jitter });\n // One poll on the database's shared heartbeat (coalesced with the reactor,\n // kv pub/sub and queue) instead of a dedicated interval.\n if (!this.task) {\n this.task = this.heartbeat.every(this.checkInterval, () => this.tick());\n }\n }\n\n /** Remove a schedule. */\n unschedule(name: string): void {\n this.handlers.delete(name);\n this.driver.prepare(`DELETE FROM _schedules WHERE name = ?`).run(name);\n }\n\n /** The next scheduled run (epoch ms) for a registered schedule. */\n next(name: string): number | undefined {\n const row = this.driver\n .prepare(`SELECT next_run FROM _schedules WHERE name = ?`)\n .get(name) as { next_run: number } | undefined;\n return row?.next_run;\n }\n\n /** @internal — exposed for tests; runs one scheduling pass. */\n tick(): void {\n const t = nowMs();\n for (const [name, reg] of this.handlers) {\n const row = this.driver\n .prepare(`SELECT next_run FROM _schedules WHERE name = ?`)\n .get(name) as { next_run: number } | undefined;\n if (!row || row.next_run > t) continue;\n const next = this.computeNext(reg.c, new Date(t), reg.tz, reg.jitter);\n // Atomic claim: only the process that flips next_run gets to fire.\n const claimed =\n this.driver\n .prepare(\n `UPDATE _schedules SET last_run = ?, next_run = ? WHERE name = ? AND next_run <= ?`,\n )\n .run(t, next, name, t).changes > 0;\n if (claimed) {\n Promise.resolve()\n .then(() => reg.fn())\n .catch((err) => this.emit(\"error\", err, name));\n }\n }\n }\n\n /** Stop the scheduler (schedules remain persisted). */\n stop(): void {\n if (this.task) this.task.cancel();\n this.task = undefined;\n }\n}\n\n/** Create a cron scheduler over a monlite database. */\nexport function createCron(db: Monlite, opts?: CronOptions): Cron {\n return new Cron(db, opts);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monlite/cron",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Cron-style job scheduling for @monlite/core: persisted schedules, 5-field cron, multi-process safe.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"node": ">=18"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@monlite/core": "^2.8.
|
|
52
|
+
"@monlite/core": "^2.8.1"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^22.10.0",
|