@monlite/cron 0.1.3 → 0.2.0

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,71 @@ 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 partsMatch(c, p) {
49
+ if (!c.minute.has(p.minute) || !c.hour.has(p.hour) || !c.month.has(p.month))
50
+ return false;
51
+ const dom = c.dom.has(p.day);
52
+ const dow = c.dow.has(p.dow);
53
+ return c.domRestricted && c.dowRestricted ? dom || dow : dom && dow;
53
54
  }
54
- function nextCronRun(expr, from = /* @__PURE__ */ new Date()) {
55
+ var localParts = (d) => ({
56
+ minute: d.getMinutes(),
57
+ hour: d.getHours(),
58
+ day: d.getDate(),
59
+ month: d.getMonth() + 1,
60
+ dow: d.getDay()
61
+ });
62
+ var DOW = {
63
+ Sun: 0,
64
+ Mon: 1,
65
+ Tue: 2,
66
+ Wed: 3,
67
+ Thu: 4,
68
+ Fri: 5,
69
+ Sat: 6
70
+ };
71
+ var tzFmtCache = /* @__PURE__ */ new Map();
72
+ function tzFormatter(tz) {
73
+ let f = tzFmtCache.get(tz);
74
+ if (!f) {
75
+ f = new Intl.DateTimeFormat("en-US", {
76
+ timeZone: tz,
77
+ hour12: false,
78
+ year: "numeric",
79
+ month: "2-digit",
80
+ day: "2-digit",
81
+ hour: "2-digit",
82
+ minute: "2-digit",
83
+ weekday: "short"
84
+ });
85
+ tzFmtCache.set(tz, f);
86
+ }
87
+ return f;
88
+ }
89
+ function tzParts(d, tz) {
90
+ const map = {};
91
+ for (const part of tzFormatter(tz).formatToParts(d))
92
+ map[part.type] = part.value;
93
+ let hour = parseInt(map.hour, 10);
94
+ if (hour === 24) hour = 0;
95
+ return {
96
+ minute: parseInt(map.minute, 10),
97
+ hour,
98
+ day: parseInt(map.day, 10),
99
+ month: parseInt(map.month, 10),
100
+ dow: DOW[map.weekday] ?? 0
101
+ };
102
+ }
103
+ function nextCronRun(expr, from = /* @__PURE__ */ new Date(), opts = {}) {
55
104
  const c = typeof expr === "string" ? parseCron(expr) : expr;
105
+ const tz = opts.tz;
56
106
  const d = new Date(from.getTime());
57
107
  d.setSeconds(0, 0);
58
108
  d.setMinutes(d.getMinutes() + 1);
59
109
  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;
62
- }
63
- d.setMinutes(d.getMinutes() + 1);
110
+ if (partsMatch(c, tz ? tzParts(d, tz) : localParts(d))) return d;
111
+ if (tz) d.setTime(d.getTime() + 6e4);
112
+ else d.setMinutes(d.getMinutes() + 1);
64
113
  }
65
114
  throw new Error(`Could not compute next run for cron "${expr}"`);
66
115
  }
@@ -87,16 +136,22 @@ var Cron = class extends events.EventEmitter {
87
136
  ensured.add(db);
88
137
  }
89
138
  }
139
+ /** Compute the next firing (epoch ms) for a schedule, applying tz + jitter. */
140
+ computeNext(c, from, tz, jitter) {
141
+ const base = nextCronRun(c, from, { tz }).getTime();
142
+ return jitter && jitter > 0 ? base + Math.floor(Math.random() * jitter) : base;
143
+ }
90
144
  /** Register (or update) a schedule and start the scheduler. */
91
- schedule(name, expr, handler) {
145
+ schedule(name, expr, handler, opts = {}) {
92
146
  const c = parseCron(expr);
147
+ const { tz, jitter } = opts;
93
148
  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();
149
+ const next = existing && existing.cron === expr ? existing.next_run : this.computeNext(c, /* @__PURE__ */ new Date(), tz, jitter);
95
150
  this.driver.prepare(
96
151
  `INSERT INTO _schedules (name, cron, next_run, last_run) VALUES (?, ?, ?, NULL)
97
152
  ON CONFLICT(name) DO UPDATE SET cron = excluded.cron, next_run = excluded.next_run`
98
153
  ).run(name, expr, next);
99
- this.handlers.set(name, { c, fn: handler });
154
+ this.handlers.set(name, { c, fn: handler, tz, jitter });
100
155
  if (!this.task) {
101
156
  this.task = this.heartbeat.every(this.checkInterval, () => this.tick());
102
157
  }
@@ -117,7 +172,7 @@ var Cron = class extends events.EventEmitter {
117
172
  for (const [name, reg] of this.handlers) {
118
173
  const row = this.driver.prepare(`SELECT next_run FROM _schedules WHERE name = ?`).get(name);
119
174
  if (!row || row.next_run > t) continue;
120
- const next = nextCronRun(reg.c, new Date(t)).getTime();
175
+ const next = this.computeNext(reg.c, new Date(t), reg.tz, reg.jitter);
121
176
  const claimed = this.driver.prepare(
122
177
  `UPDATE _schedules SET last_run = ?, next_run = ? WHERE name = ? AND next_run <= ?`
123
178
  ).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\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"]}
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,71 @@ 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 partsMatch(c, p) {
47
+ if (!c.minute.has(p.minute) || !c.hour.has(p.hour) || !c.month.has(p.month))
48
+ return false;
49
+ const dom = c.dom.has(p.day);
50
+ const dow = c.dow.has(p.dow);
51
+ return c.domRestricted && c.dowRestricted ? dom || dow : dom && dow;
51
52
  }
52
- function nextCronRun(expr, from = /* @__PURE__ */ new Date()) {
53
+ var localParts = (d) => ({
54
+ minute: d.getMinutes(),
55
+ hour: d.getHours(),
56
+ day: d.getDate(),
57
+ month: d.getMonth() + 1,
58
+ dow: d.getDay()
59
+ });
60
+ var DOW = {
61
+ Sun: 0,
62
+ Mon: 1,
63
+ Tue: 2,
64
+ Wed: 3,
65
+ Thu: 4,
66
+ Fri: 5,
67
+ Sat: 6
68
+ };
69
+ var tzFmtCache = /* @__PURE__ */ new Map();
70
+ function tzFormatter(tz) {
71
+ let f = tzFmtCache.get(tz);
72
+ if (!f) {
73
+ f = new Intl.DateTimeFormat("en-US", {
74
+ timeZone: tz,
75
+ hour12: false,
76
+ year: "numeric",
77
+ month: "2-digit",
78
+ day: "2-digit",
79
+ hour: "2-digit",
80
+ minute: "2-digit",
81
+ weekday: "short"
82
+ });
83
+ tzFmtCache.set(tz, f);
84
+ }
85
+ return f;
86
+ }
87
+ function tzParts(d, tz) {
88
+ const map = {};
89
+ for (const part of tzFormatter(tz).formatToParts(d))
90
+ map[part.type] = part.value;
91
+ let hour = parseInt(map.hour, 10);
92
+ if (hour === 24) hour = 0;
93
+ return {
94
+ minute: parseInt(map.minute, 10),
95
+ hour,
96
+ day: parseInt(map.day, 10),
97
+ month: parseInt(map.month, 10),
98
+ dow: DOW[map.weekday] ?? 0
99
+ };
100
+ }
101
+ function nextCronRun(expr, from = /* @__PURE__ */ new Date(), opts = {}) {
53
102
  const c = typeof expr === "string" ? parseCron(expr) : expr;
103
+ const tz = opts.tz;
54
104
  const d = new Date(from.getTime());
55
105
  d.setSeconds(0, 0);
56
106
  d.setMinutes(d.getMinutes() + 1);
57
107
  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;
60
- }
61
- d.setMinutes(d.getMinutes() + 1);
108
+ if (partsMatch(c, tz ? tzParts(d, tz) : localParts(d))) return d;
109
+ if (tz) d.setTime(d.getTime() + 6e4);
110
+ else d.setMinutes(d.getMinutes() + 1);
62
111
  }
63
112
  throw new Error(`Could not compute next run for cron "${expr}"`);
64
113
  }
@@ -85,16 +134,22 @@ var Cron = class extends EventEmitter {
85
134
  ensured.add(db);
86
135
  }
87
136
  }
137
+ /** Compute the next firing (epoch ms) for a schedule, applying tz + jitter. */
138
+ computeNext(c, from, tz, jitter) {
139
+ const base = nextCronRun(c, from, { tz }).getTime();
140
+ return jitter && jitter > 0 ? base + Math.floor(Math.random() * jitter) : base;
141
+ }
88
142
  /** Register (or update) a schedule and start the scheduler. */
89
- schedule(name, expr, handler) {
143
+ schedule(name, expr, handler, opts = {}) {
90
144
  const c = parseCron(expr);
145
+ const { tz, jitter } = opts;
91
146
  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();
147
+ const next = existing && existing.cron === expr ? existing.next_run : this.computeNext(c, /* @__PURE__ */ new Date(), tz, jitter);
93
148
  this.driver.prepare(
94
149
  `INSERT INTO _schedules (name, cron, next_run, last_run) VALUES (?, ?, ?, NULL)
95
150
  ON CONFLICT(name) DO UPDATE SET cron = excluded.cron, next_run = excluded.next_run`
96
151
  ).run(name, expr, next);
97
- this.handlers.set(name, { c, fn: handler });
152
+ this.handlers.set(name, { c, fn: handler, tz, jitter });
98
153
  if (!this.task) {
99
154
  this.task = this.heartbeat.every(this.checkInterval, () => this.tick());
100
155
  }
@@ -115,7 +170,7 @@ var Cron = class extends EventEmitter {
115
170
  for (const [name, reg] of this.handlers) {
116
171
  const row = this.driver.prepare(`SELECT next_run FROM _schedules WHERE name = ?`).get(name);
117
172
  if (!row || row.next_run > t) continue;
118
- const next = nextCronRun(reg.c, new Date(t)).getTime();
173
+ const next = this.computeNext(reg.c, new Date(t), reg.tz, reg.jitter);
119
174
  const claimed = this.driver.prepare(
120
175
  `UPDATE _schedules SET last_run = ?, next_run = ? WHERE name = ? AND next_run <= ?`
121
176
  ).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\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monlite/cron",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
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",