@monlite/cron 0.1.2 → 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 +76 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -5
- package/dist/index.d.ts +28 -5
- package/dist/index.js +76 -20
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -45,22 +45,71 @@ function parseCron(expr) {
|
|
|
45
45
|
dowRestricted: parts[4] !== "*"
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
|
61
|
-
|
|
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
|
}
|
|
@@ -68,12 +117,14 @@ var ensured = /* @__PURE__ */ new WeakSet();
|
|
|
68
117
|
var nowMs = () => Date.now();
|
|
69
118
|
var Cron = class extends events.EventEmitter {
|
|
70
119
|
driver;
|
|
120
|
+
heartbeat;
|
|
71
121
|
checkInterval;
|
|
72
122
|
handlers = /* @__PURE__ */ new Map();
|
|
73
|
-
|
|
123
|
+
task;
|
|
74
124
|
constructor(db, opts = {}) {
|
|
75
125
|
super();
|
|
76
126
|
this.driver = db.driver;
|
|
127
|
+
this.heartbeat = db.heartbeat;
|
|
77
128
|
this.checkInterval = opts.checkInterval ?? 1e3;
|
|
78
129
|
if (!ensured.has(db)) {
|
|
79
130
|
this.driver.exec(
|
|
@@ -85,19 +136,24 @@ var Cron = class extends events.EventEmitter {
|
|
|
85
136
|
ensured.add(db);
|
|
86
137
|
}
|
|
87
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
|
+
}
|
|
88
144
|
/** Register (or update) a schedule and start the scheduler. */
|
|
89
|
-
schedule(name, expr, handler) {
|
|
145
|
+
schedule(name, expr, handler, opts = {}) {
|
|
90
146
|
const c = parseCron(expr);
|
|
147
|
+
const { tz, jitter } = opts;
|
|
91
148
|
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 :
|
|
149
|
+
const next = existing && existing.cron === expr ? existing.next_run : this.computeNext(c, /* @__PURE__ */ new Date(), tz, jitter);
|
|
93
150
|
this.driver.prepare(
|
|
94
151
|
`INSERT INTO _schedules (name, cron, next_run, last_run) VALUES (?, ?, ?, NULL)
|
|
95
152
|
ON CONFLICT(name) DO UPDATE SET cron = excluded.cron, next_run = excluded.next_run`
|
|
96
153
|
).run(name, expr, next);
|
|
97
|
-
this.handlers.set(name, { c, fn: handler });
|
|
98
|
-
if (!this.
|
|
99
|
-
this.
|
|
100
|
-
this.timer.unref?.();
|
|
154
|
+
this.handlers.set(name, { c, fn: handler, tz, jitter });
|
|
155
|
+
if (!this.task) {
|
|
156
|
+
this.task = this.heartbeat.every(this.checkInterval, () => this.tick());
|
|
101
157
|
}
|
|
102
158
|
}
|
|
103
159
|
/** Remove a schedule. */
|
|
@@ -116,7 +172,7 @@ var Cron = class extends events.EventEmitter {
|
|
|
116
172
|
for (const [name, reg] of this.handlers) {
|
|
117
173
|
const row = this.driver.prepare(`SELECT next_run FROM _schedules WHERE name = ?`).get(name);
|
|
118
174
|
if (!row || row.next_run > t) continue;
|
|
119
|
-
const next =
|
|
175
|
+
const next = this.computeNext(reg.c, new Date(t), reg.tz, reg.jitter);
|
|
120
176
|
const claimed = this.driver.prepare(
|
|
121
177
|
`UPDATE _schedules SET last_run = ?, next_run = ? WHERE name = ? AND next_run <= ?`
|
|
122
178
|
).run(t, next, name, t).changes > 0;
|
|
@@ -127,8 +183,8 @@ var Cron = class extends events.EventEmitter {
|
|
|
127
183
|
}
|
|
128
184
|
/** Stop the scheduler (schedules remain persisted). */
|
|
129
185
|
stop() {
|
|
130
|
-
if (this.
|
|
131
|
-
this.
|
|
186
|
+
if (this.task) this.task.cancel();
|
|
187
|
+
this.task = void 0;
|
|
132
188
|
}
|
|
133
189
|
};
|
|
134
190
|
function createCron(db, opts) {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["EventEmitter"],"mappings":"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","file":"index.cjs","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport type { Monlite } 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 checkInterval: number;\n private readonly handlers = new Map<\n string,\n { c: ParsedCron; fn: CronHandler }\n >();\n private timer: ReturnType<typeof setInterval> | undefined;\n\n constructor(db: Monlite, opts: CronOptions = {}) {\n super();\n this.driver = db.driver;\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 if (!this.timer) {\n this.timer = setInterval(() => this.tick(), this.checkInterval);\n this.timer.unref?.();\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.timer) clearInterval(this.timer);\n this.timer = 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
|
-
/**
|
|
21
|
-
|
|
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.
|
|
@@ -27,12 +47,15 @@ declare function nextCronRun(expr: string | ParsedCron, from?: Date): Date;
|
|
|
27
47
|
*/
|
|
28
48
|
declare class Cron extends EventEmitter {
|
|
29
49
|
private readonly driver;
|
|
50
|
+
private readonly heartbeat;
|
|
30
51
|
private readonly checkInterval;
|
|
31
52
|
private readonly handlers;
|
|
32
|
-
private
|
|
53
|
+
private task;
|
|
33
54
|
constructor(db: Monlite, opts?: CronOptions);
|
|
55
|
+
/** Compute the next firing (epoch ms) for a schedule, applying tz + jitter. */
|
|
56
|
+
private computeNext;
|
|
34
57
|
/** Register (or update) a schedule and start the scheduler. */
|
|
35
|
-
schedule(name: string, expr: string, handler: CronHandler): void;
|
|
58
|
+
schedule(name: string, expr: string, handler: CronHandler, opts?: ScheduleOptions): void;
|
|
36
59
|
/** Remove a schedule. */
|
|
37
60
|
unschedule(name: string): void;
|
|
38
61
|
/** The next scheduled run (epoch ms) for a registered schedule. */
|
|
@@ -45,4 +68,4 @@ declare class Cron extends EventEmitter {
|
|
|
45
68
|
/** Create a cron scheduler over a monlite database. */
|
|
46
69
|
declare function createCron(db: Monlite, opts?: CronOptions): Cron;
|
|
47
70
|
|
|
48
|
-
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
|
-
/**
|
|
21
|
-
|
|
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.
|
|
@@ -27,12 +47,15 @@ declare function nextCronRun(expr: string | ParsedCron, from?: Date): Date;
|
|
|
27
47
|
*/
|
|
28
48
|
declare class Cron extends EventEmitter {
|
|
29
49
|
private readonly driver;
|
|
50
|
+
private readonly heartbeat;
|
|
30
51
|
private readonly checkInterval;
|
|
31
52
|
private readonly handlers;
|
|
32
|
-
private
|
|
53
|
+
private task;
|
|
33
54
|
constructor(db: Monlite, opts?: CronOptions);
|
|
55
|
+
/** Compute the next firing (epoch ms) for a schedule, applying tz + jitter. */
|
|
56
|
+
private computeNext;
|
|
34
57
|
/** Register (or update) a schedule and start the scheduler. */
|
|
35
|
-
schedule(name: string, expr: string, handler: CronHandler): void;
|
|
58
|
+
schedule(name: string, expr: string, handler: CronHandler, opts?: ScheduleOptions): void;
|
|
36
59
|
/** Remove a schedule. */
|
|
37
60
|
unschedule(name: string): void;
|
|
38
61
|
/** The next scheduled run (epoch ms) for a registered schedule. */
|
|
@@ -45,4 +68,4 @@ declare class Cron extends EventEmitter {
|
|
|
45
68
|
/** Create a cron scheduler over a monlite database. */
|
|
46
69
|
declare function createCron(db: Monlite, opts?: CronOptions): Cron;
|
|
47
70
|
|
|
48
|
-
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
59
|
-
|
|
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
|
}
|
|
@@ -66,12 +115,14 @@ var ensured = /* @__PURE__ */ new WeakSet();
|
|
|
66
115
|
var nowMs = () => Date.now();
|
|
67
116
|
var Cron = class extends EventEmitter {
|
|
68
117
|
driver;
|
|
118
|
+
heartbeat;
|
|
69
119
|
checkInterval;
|
|
70
120
|
handlers = /* @__PURE__ */ new Map();
|
|
71
|
-
|
|
121
|
+
task;
|
|
72
122
|
constructor(db, opts = {}) {
|
|
73
123
|
super();
|
|
74
124
|
this.driver = db.driver;
|
|
125
|
+
this.heartbeat = db.heartbeat;
|
|
75
126
|
this.checkInterval = opts.checkInterval ?? 1e3;
|
|
76
127
|
if (!ensured.has(db)) {
|
|
77
128
|
this.driver.exec(
|
|
@@ -83,19 +134,24 @@ var Cron = class extends EventEmitter {
|
|
|
83
134
|
ensured.add(db);
|
|
84
135
|
}
|
|
85
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
|
+
}
|
|
86
142
|
/** Register (or update) a schedule and start the scheduler. */
|
|
87
|
-
schedule(name, expr, handler) {
|
|
143
|
+
schedule(name, expr, handler, opts = {}) {
|
|
88
144
|
const c = parseCron(expr);
|
|
145
|
+
const { tz, jitter } = opts;
|
|
89
146
|
const existing = this.driver.prepare(`SELECT next_run, cron FROM _schedules WHERE name = ?`).get(name);
|
|
90
|
-
const next = existing && existing.cron === expr ? existing.next_run :
|
|
147
|
+
const next = existing && existing.cron === expr ? existing.next_run : this.computeNext(c, /* @__PURE__ */ new Date(), tz, jitter);
|
|
91
148
|
this.driver.prepare(
|
|
92
149
|
`INSERT INTO _schedules (name, cron, next_run, last_run) VALUES (?, ?, ?, NULL)
|
|
93
150
|
ON CONFLICT(name) DO UPDATE SET cron = excluded.cron, next_run = excluded.next_run`
|
|
94
151
|
).run(name, expr, next);
|
|
95
|
-
this.handlers.set(name, { c, fn: handler });
|
|
96
|
-
if (!this.
|
|
97
|
-
this.
|
|
98
|
-
this.timer.unref?.();
|
|
152
|
+
this.handlers.set(name, { c, fn: handler, tz, jitter });
|
|
153
|
+
if (!this.task) {
|
|
154
|
+
this.task = this.heartbeat.every(this.checkInterval, () => this.tick());
|
|
99
155
|
}
|
|
100
156
|
}
|
|
101
157
|
/** Remove a schedule. */
|
|
@@ -114,7 +170,7 @@ var Cron = class extends EventEmitter {
|
|
|
114
170
|
for (const [name, reg] of this.handlers) {
|
|
115
171
|
const row = this.driver.prepare(`SELECT next_run FROM _schedules WHERE name = ?`).get(name);
|
|
116
172
|
if (!row || row.next_run > t) continue;
|
|
117
|
-
const next =
|
|
173
|
+
const next = this.computeNext(reg.c, new Date(t), reg.tz, reg.jitter);
|
|
118
174
|
const claimed = this.driver.prepare(
|
|
119
175
|
`UPDATE _schedules SET last_run = ?, next_run = ? WHERE name = ? AND next_run <= ?`
|
|
120
176
|
).run(t, next, name, t).changes > 0;
|
|
@@ -125,8 +181,8 @@ var Cron = class extends EventEmitter {
|
|
|
125
181
|
}
|
|
126
182
|
/** Stop the scheduler (schedules remain persisted). */
|
|
127
183
|
stop() {
|
|
128
|
-
if (this.
|
|
129
|
-
this.
|
|
184
|
+
if (this.task) this.task.cancel();
|
|
185
|
+
this.task = void 0;
|
|
130
186
|
}
|
|
131
187
|
};
|
|
132
188
|
function createCron(db, opts) {
|
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 } 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 checkInterval: number;\n private readonly handlers = new Map<\n string,\n { c: ParsedCron; fn: CronHandler }\n >();\n private timer: ReturnType<typeof setInterval> | undefined;\n\n constructor(db: Monlite, opts: CronOptions = {}) {\n super();\n this.driver = db.driver;\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 if (!this.timer) {\n this.timer = setInterval(() => this.tick(), this.checkInterval);\n this.timer.unref?.();\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.timer) clearInterval(this.timer);\n this.timer = 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.
|
|
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",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"node": ">=18"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@monlite/core": "^2.
|
|
52
|
+
"@monlite/core": "^2.8.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^22.10.0",
|