@monlite/cron 0.1.3 → 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 CHANGED
@@ -45,22 +45,83 @@ function parseCron(expr) {
45
45
  dowRestricted: parts[4] !== "*"
46
46
  };
47
47
  }
48
- function dayMatches(c, d) {
49
- const dom = c.dom.has(d.getDate());
50
- const dow = c.dow.has(d.getDay());
51
- if (c.domRestricted && c.dowRestricted) return dom || dow;
52
- return dom && dow;
48
+ function dayMatches(c, p) {
49
+ if (!c.month.has(p.month)) return false;
50
+ const dom = c.dom.has(p.day);
51
+ const dow = c.dow.has(p.dow);
52
+ return c.domRestricted && c.dowRestricted ? dom || dow : dom && dow;
53
53
  }
54
- function nextCronRun(expr, from = /* @__PURE__ */ new Date()) {
54
+ var localParts = (d) => ({
55
+ minute: d.getMinutes(),
56
+ hour: d.getHours(),
57
+ day: d.getDate(),
58
+ month: d.getMonth() + 1,
59
+ dow: d.getDay()
60
+ });
61
+ var DOW = {
62
+ Sun: 0,
63
+ Mon: 1,
64
+ Tue: 2,
65
+ Wed: 3,
66
+ Thu: 4,
67
+ Fri: 5,
68
+ Sat: 6
69
+ };
70
+ var tzFmtCache = /* @__PURE__ */ new Map();
71
+ function tzFormatter(tz) {
72
+ let f = tzFmtCache.get(tz);
73
+ if (!f) {
74
+ f = new Intl.DateTimeFormat("en-US", {
75
+ timeZone: tz,
76
+ hour12: false,
77
+ year: "numeric",
78
+ month: "2-digit",
79
+ day: "2-digit",
80
+ hour: "2-digit",
81
+ minute: "2-digit",
82
+ weekday: "short"
83
+ });
84
+ tzFmtCache.set(tz, f);
85
+ }
86
+ return f;
87
+ }
88
+ function tzParts(d, tz) {
89
+ const map = {};
90
+ for (const part of tzFormatter(tz).formatToParts(d))
91
+ map[part.type] = part.value;
92
+ let hour = parseInt(map.hour, 10);
93
+ if (hour === 24) hour = 0;
94
+ return {
95
+ minute: parseInt(map.minute, 10),
96
+ hour,
97
+ day: parseInt(map.day, 10),
98
+ month: parseInt(map.month, 10),
99
+ dow: DOW[map.weekday] ?? 0
100
+ };
101
+ }
102
+ function nextCronRun(expr, from = /* @__PURE__ */ new Date(), opts = {}) {
55
103
  const c = typeof expr === "string" ? parseCron(expr) : expr;
104
+ const tz = opts.tz;
56
105
  const d = new Date(from.getTime());
57
106
  d.setSeconds(0, 0);
58
107
  d.setMinutes(d.getMinutes() + 1);
59
- for (let i = 0; i < 5 * 366 * 24 * 60; i++) {
60
- if (c.minute.has(d.getMinutes()) && c.hour.has(d.getHours()) && c.month.has(d.getMonth() + 1) && dayMatches(c, d)) {
61
- return d;
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;
62
116
  }
63
- d.setMinutes(d.getMinutes() + 1);
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;
123
+ if (tz) d.setTime(d.getTime() + 6e4);
124
+ else d.setMinutes(d.getMinutes() + 1);
64
125
  }
65
126
  throw new Error(`Could not compute next run for cron "${expr}"`);
66
127
  }
@@ -87,16 +148,22 @@ var Cron = class extends events.EventEmitter {
87
148
  ensured.add(db);
88
149
  }
89
150
  }
151
+ /** Compute the next firing (epoch ms) for a schedule, applying tz + jitter. */
152
+ computeNext(c, from, tz, jitter) {
153
+ const base = nextCronRun(c, from, { tz }).getTime();
154
+ return jitter && jitter > 0 ? base + Math.floor(Math.random() * jitter) : base;
155
+ }
90
156
  /** Register (or update) a schedule and start the scheduler. */
91
- schedule(name, expr, handler) {
157
+ schedule(name, expr, handler, opts = {}) {
92
158
  const c = parseCron(expr);
159
+ const { tz, jitter } = opts;
93
160
  const existing = this.driver.prepare(`SELECT next_run, cron FROM _schedules WHERE name = ?`).get(name);
94
- const next = existing && existing.cron === expr ? existing.next_run : nextCronRun(c).getTime();
161
+ const next = existing && existing.cron === expr ? existing.next_run : this.computeNext(c, /* @__PURE__ */ new Date(), tz, jitter);
95
162
  this.driver.prepare(
96
163
  `INSERT INTO _schedules (name, cron, next_run, last_run) VALUES (?, ?, ?, NULL)
97
164
  ON CONFLICT(name) DO UPDATE SET cron = excluded.cron, next_run = excluded.next_run`
98
165
  ).run(name, expr, next);
99
- this.handlers.set(name, { c, fn: handler });
166
+ this.handlers.set(name, { c, fn: handler, tz, jitter });
100
167
  if (!this.task) {
101
168
  this.task = this.heartbeat.every(this.checkInterval, () => this.tick());
102
169
  }
@@ -117,7 +184,7 @@ var Cron = class extends events.EventEmitter {
117
184
  for (const [name, reg] of this.handlers) {
118
185
  const row = this.driver.prepare(`SELECT next_run FROM _schedules WHERE name = ?`).get(name);
119
186
  if (!row || row.next_run > t) continue;
120
- const next = nextCronRun(reg.c, new Date(t)).getTime();
187
+ const next = this.computeNext(reg.c, new Date(t), reg.tz, reg.jitter);
121
188
  const claimed = this.driver.prepare(
122
189
  `UPDATE _schedules SET last_run = ?, next_run = ? WHERE name = ? AND next_run <= ?`
123
190
  ).run(t, next, name, t).changes > 0;
@@ -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\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\nfunction dayMatches(c: ParsedCron, d: Date): boolean {\n const dom = c.dom.has(d.getDate());\n const dow = c.dow.has(d.getDay()); // 0 = Sunday\n // POSIX: when both day-of-month and day-of-week are restricted, either matches.\n if (c.domRestricted && c.dowRestricted) return dom || dow;\n return dom && dow;\n}\n\n/** The next time (strictly after `from`, local time) a cron expression fires. */\nexport function nextCronRun(\n expr: string | ParsedCron,\n from: Date = new Date(),\n): Date {\n const c = typeof expr === \"string\" ? parseCron(expr) : expr;\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. JS Date arithmetic below skips\n // non-existent days (Feb 29 in common years) and handles DST transitions natively.\n for (let i = 0; i < 5 * 366 * 24 * 60; i++) {\n if (\n c.minute.has(d.getMinutes()) &&\n c.hour.has(d.getHours()) &&\n c.month.has(d.getMonth() + 1) &&\n dayMatches(c, d)\n ) {\n return d;\n }\n 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 }\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 /** Register (or update) a schedule and start the scheduler. */\n schedule(name: string, expr: string, handler: CronHandler): void {\n const c = parseCron(expr);\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 : nextCronRun(c).getTime();\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 });\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 = nextCronRun(reg.c, new Date(t)).getTime();\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.d.cts CHANGED
@@ -5,6 +5,20 @@ interface CronOptions {
5
5
  /** How often the scheduler checks for due jobs (ms). Default 1000. */
6
6
  checkInterval?: number;
7
7
  }
8
+ /** Per-schedule options. */
9
+ interface ScheduleOptions {
10
+ /**
11
+ * IANA time zone (e.g. `"Europe/Istanbul"`) the cron expression is evaluated
12
+ * in, DST included. Default: the server's local time.
13
+ */
14
+ tz?: string;
15
+ /**
16
+ * Add a random delay of up to this many ms to each firing — spreads a
17
+ * thundering herd of schedules that would otherwise fire at the same instant.
18
+ * Default `0`.
19
+ */
20
+ jitter?: number;
21
+ }
8
22
  type CronHandler = () => void | Promise<void>;
9
23
  interface ParsedCron {
10
24
  minute: Set<number>;
@@ -17,8 +31,14 @@ interface ParsedCron {
17
31
  }
18
32
  /** Parse a standard 5-field cron expression (`min hour dom month dow`). */
19
33
  declare function parseCron(expr: string): ParsedCron;
20
- /** The next time (strictly after `from`, local time) a cron expression fires. */
21
- declare function nextCronRun(expr: string | ParsedCron, from?: Date): Date;
34
+ /**
35
+ * The next time (strictly after `from`) a cron expression fires. Evaluated in
36
+ * local time by default, or in `opts.tz` (an IANA zone like `"Europe/Istanbul"`)
37
+ * — handling that zone's DST transitions.
38
+ */
39
+ declare function nextCronRun(expr: string | ParsedCron, from?: Date, opts?: {
40
+ tz?: string;
41
+ }): Date;
22
42
  /**
23
43
  * A persisted cron scheduler. Schedules survive restarts (next-run is stored),
24
44
  * and firing is atomic so multiple processes won't double-run an occurrence.
@@ -32,8 +52,10 @@ declare class Cron extends EventEmitter {
32
52
  private readonly handlers;
33
53
  private task;
34
54
  constructor(db: Monlite, opts?: CronOptions);
55
+ /** Compute the next firing (epoch ms) for a schedule, applying tz + jitter. */
56
+ private computeNext;
35
57
  /** Register (or update) a schedule and start the scheduler. */
36
- schedule(name: string, expr: string, handler: CronHandler): void;
58
+ schedule(name: string, expr: string, handler: CronHandler, opts?: ScheduleOptions): void;
37
59
  /** Remove a schedule. */
38
60
  unschedule(name: string): void;
39
61
  /** The next scheduled run (epoch ms) for a registered schedule. */
@@ -46,4 +68,4 @@ declare class Cron extends EventEmitter {
46
68
  /** Create a cron scheduler over a monlite database. */
47
69
  declare function createCron(db: Monlite, opts?: CronOptions): Cron;
48
70
 
49
- export { Cron, type CronHandler, type CronOptions, type ParsedCron, createCron, nextCronRun, parseCron };
71
+ export { Cron, type CronHandler, type CronOptions, type ParsedCron, type ScheduleOptions, createCron, nextCronRun, parseCron };
package/dist/index.d.ts CHANGED
@@ -5,6 +5,20 @@ interface CronOptions {
5
5
  /** How often the scheduler checks for due jobs (ms). Default 1000. */
6
6
  checkInterval?: number;
7
7
  }
8
+ /** Per-schedule options. */
9
+ interface ScheduleOptions {
10
+ /**
11
+ * IANA time zone (e.g. `"Europe/Istanbul"`) the cron expression is evaluated
12
+ * in, DST included. Default: the server's local time.
13
+ */
14
+ tz?: string;
15
+ /**
16
+ * Add a random delay of up to this many ms to each firing — spreads a
17
+ * thundering herd of schedules that would otherwise fire at the same instant.
18
+ * Default `0`.
19
+ */
20
+ jitter?: number;
21
+ }
8
22
  type CronHandler = () => void | Promise<void>;
9
23
  interface ParsedCron {
10
24
  minute: Set<number>;
@@ -17,8 +31,14 @@ interface ParsedCron {
17
31
  }
18
32
  /** Parse a standard 5-field cron expression (`min hour dom month dow`). */
19
33
  declare function parseCron(expr: string): ParsedCron;
20
- /** The next time (strictly after `from`, local time) a cron expression fires. */
21
- declare function nextCronRun(expr: string | ParsedCron, from?: Date): Date;
34
+ /**
35
+ * The next time (strictly after `from`) a cron expression fires. Evaluated in
36
+ * local time by default, or in `opts.tz` (an IANA zone like `"Europe/Istanbul"`)
37
+ * — handling that zone's DST transitions.
38
+ */
39
+ declare function nextCronRun(expr: string | ParsedCron, from?: Date, opts?: {
40
+ tz?: string;
41
+ }): Date;
22
42
  /**
23
43
  * A persisted cron scheduler. Schedules survive restarts (next-run is stored),
24
44
  * and firing is atomic so multiple processes won't double-run an occurrence.
@@ -32,8 +52,10 @@ declare class Cron extends EventEmitter {
32
52
  private readonly handlers;
33
53
  private task;
34
54
  constructor(db: Monlite, opts?: CronOptions);
55
+ /** Compute the next firing (epoch ms) for a schedule, applying tz + jitter. */
56
+ private computeNext;
35
57
  /** Register (or update) a schedule and start the scheduler. */
36
- schedule(name: string, expr: string, handler: CronHandler): void;
58
+ schedule(name: string, expr: string, handler: CronHandler, opts?: ScheduleOptions): void;
37
59
  /** Remove a schedule. */
38
60
  unschedule(name: string): void;
39
61
  /** The next scheduled run (epoch ms) for a registered schedule. */
@@ -46,4 +68,4 @@ declare class Cron extends EventEmitter {
46
68
  /** Create a cron scheduler over a monlite database. */
47
69
  declare function createCron(db: Monlite, opts?: CronOptions): Cron;
48
70
 
49
- export { Cron, type CronHandler, type CronOptions, type ParsedCron, createCron, nextCronRun, parseCron };
71
+ export { Cron, type CronHandler, type CronOptions, type ParsedCron, type ScheduleOptions, createCron, nextCronRun, parseCron };
package/dist/index.js CHANGED
@@ -43,22 +43,83 @@ function parseCron(expr) {
43
43
  dowRestricted: parts[4] !== "*"
44
44
  };
45
45
  }
46
- function dayMatches(c, d) {
47
- const dom = c.dom.has(d.getDate());
48
- const dow = c.dow.has(d.getDay());
49
- if (c.domRestricted && c.dowRestricted) return dom || dow;
50
- return dom && dow;
46
+ function dayMatches(c, p) {
47
+ if (!c.month.has(p.month)) return false;
48
+ const dom = c.dom.has(p.day);
49
+ const dow = c.dow.has(p.dow);
50
+ return c.domRestricted && c.dowRestricted ? dom || dow : dom && dow;
51
51
  }
52
- function nextCronRun(expr, from = /* @__PURE__ */ new Date()) {
52
+ var localParts = (d) => ({
53
+ minute: d.getMinutes(),
54
+ hour: d.getHours(),
55
+ day: d.getDate(),
56
+ month: d.getMonth() + 1,
57
+ dow: d.getDay()
58
+ });
59
+ var DOW = {
60
+ Sun: 0,
61
+ Mon: 1,
62
+ Tue: 2,
63
+ Wed: 3,
64
+ Thu: 4,
65
+ Fri: 5,
66
+ Sat: 6
67
+ };
68
+ var tzFmtCache = /* @__PURE__ */ new Map();
69
+ function tzFormatter(tz) {
70
+ let f = tzFmtCache.get(tz);
71
+ if (!f) {
72
+ f = new Intl.DateTimeFormat("en-US", {
73
+ timeZone: tz,
74
+ hour12: false,
75
+ year: "numeric",
76
+ month: "2-digit",
77
+ day: "2-digit",
78
+ hour: "2-digit",
79
+ minute: "2-digit",
80
+ weekday: "short"
81
+ });
82
+ tzFmtCache.set(tz, f);
83
+ }
84
+ return f;
85
+ }
86
+ function tzParts(d, tz) {
87
+ const map = {};
88
+ for (const part of tzFormatter(tz).formatToParts(d))
89
+ map[part.type] = part.value;
90
+ let hour = parseInt(map.hour, 10);
91
+ if (hour === 24) hour = 0;
92
+ return {
93
+ minute: parseInt(map.minute, 10),
94
+ hour,
95
+ day: parseInt(map.day, 10),
96
+ month: parseInt(map.month, 10),
97
+ dow: DOW[map.weekday] ?? 0
98
+ };
99
+ }
100
+ function nextCronRun(expr, from = /* @__PURE__ */ new Date(), opts = {}) {
53
101
  const c = typeof expr === "string" ? parseCron(expr) : expr;
102
+ const tz = opts.tz;
54
103
  const d = new Date(from.getTime());
55
104
  d.setSeconds(0, 0);
56
105
  d.setMinutes(d.getMinutes() + 1);
57
- for (let i = 0; i < 5 * 366 * 24 * 60; i++) {
58
- if (c.minute.has(d.getMinutes()) && c.hour.has(d.getHours()) && c.month.has(d.getMonth() + 1) && dayMatches(c, d)) {
59
- return d;
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;
60
114
  }
61
- d.setMinutes(d.getMinutes() + 1);
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;
121
+ if (tz) d.setTime(d.getTime() + 6e4);
122
+ else d.setMinutes(d.getMinutes() + 1);
62
123
  }
63
124
  throw new Error(`Could not compute next run for cron "${expr}"`);
64
125
  }
@@ -85,16 +146,22 @@ var Cron = class extends EventEmitter {
85
146
  ensured.add(db);
86
147
  }
87
148
  }
149
+ /** Compute the next firing (epoch ms) for a schedule, applying tz + jitter. */
150
+ computeNext(c, from, tz, jitter) {
151
+ const base = nextCronRun(c, from, { tz }).getTime();
152
+ return jitter && jitter > 0 ? base + Math.floor(Math.random() * jitter) : base;
153
+ }
88
154
  /** Register (or update) a schedule and start the scheduler. */
89
- schedule(name, expr, handler) {
155
+ schedule(name, expr, handler, opts = {}) {
90
156
  const c = parseCron(expr);
157
+ const { tz, jitter } = opts;
91
158
  const existing = this.driver.prepare(`SELECT next_run, cron FROM _schedules WHERE name = ?`).get(name);
92
- const next = existing && existing.cron === expr ? existing.next_run : nextCronRun(c).getTime();
159
+ const next = existing && existing.cron === expr ? existing.next_run : this.computeNext(c, /* @__PURE__ */ new Date(), tz, jitter);
93
160
  this.driver.prepare(
94
161
  `INSERT INTO _schedules (name, cron, next_run, last_run) VALUES (?, ?, ?, NULL)
95
162
  ON CONFLICT(name) DO UPDATE SET cron = excluded.cron, next_run = excluded.next_run`
96
163
  ).run(name, expr, next);
97
- this.handlers.set(name, { c, fn: handler });
164
+ this.handlers.set(name, { c, fn: handler, tz, jitter });
98
165
  if (!this.task) {
99
166
  this.task = this.heartbeat.every(this.checkInterval, () => this.tick());
100
167
  }
@@ -115,7 +182,7 @@ var Cron = class extends EventEmitter {
115
182
  for (const [name, reg] of this.handlers) {
116
183
  const row = this.driver.prepare(`SELECT next_run FROM _schedules WHERE name = ?`).get(name);
117
184
  if (!row || row.next_run > t) continue;
118
- const next = nextCronRun(reg.c, new Date(t)).getTime();
185
+ const next = this.computeNext(reg.c, new Date(t), reg.tz, reg.jitter);
119
186
  const claimed = this.driver.prepare(
120
187
  `UPDATE _schedules SET last_run = ?, next_run = ? WHERE name = ? AND next_run <= ?`
121
188
  ).run(t, next, name, t).changes > 0;
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\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\nfunction dayMatches(c: ParsedCron, d: Date): boolean {\n const dom = c.dom.has(d.getDate());\n const dow = c.dow.has(d.getDay()); // 0 = Sunday\n // POSIX: when both day-of-month and day-of-week are restricted, either matches.\n if (c.domRestricted && c.dowRestricted) return dom || dow;\n return dom && dow;\n}\n\n/** The next time (strictly after `from`, local time) a cron expression fires. */\nexport function nextCronRun(\n expr: string | ParsedCron,\n from: Date = new Date(),\n): Date {\n const c = typeof expr === \"string\" ? parseCron(expr) : expr;\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. JS Date arithmetic below skips\n // non-existent days (Feb 29 in common years) and handles DST transitions natively.\n for (let i = 0; i < 5 * 366 * 24 * 60; i++) {\n if (\n c.minute.has(d.getMinutes()) &&\n c.hour.has(d.getHours()) &&\n c.month.has(d.getMonth() + 1) &&\n dayMatches(c, d)\n ) {\n return d;\n }\n 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 }\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 /** Register (or update) a schedule and start the scheduler. */\n schedule(name: string, expr: string, handler: CronHandler): void {\n const c = parseCron(expr);\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 : nextCronRun(c).getTime();\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 });\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 = nextCronRun(reg.c, new Date(t)).getTime();\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.1.3",
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.0"
52
+ "@monlite/core": "^2.8.1"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/node": "^22.10.0",