@monlite/cron 0.1.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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/index.cjs +142 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +137 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Emad Jumaah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @monlite/cron
|
|
2
|
+
|
|
3
|
+
**Cron-style scheduling** for [`@monlite/core`](https://www.npmjs.com/package/@monlite/core), backed by SQLite. Persisted schedules (survive restarts), a zero-dependency 5-field cron parser, and atomic firing so multiple processes won't double-run. Part of the [local AI-agent harness](https://github.com/qataruts/monlite#readme).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @monlite/core @monlite/cron
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createDb } from "@monlite/core";
|
|
13
|
+
import { createCron } from "@monlite/cron";
|
|
14
|
+
|
|
15
|
+
const db = createDb("app.db");
|
|
16
|
+
const cron = createCron(db);
|
|
17
|
+
|
|
18
|
+
cron.schedule("cleanup", "0 3 * * *", async () => {
|
|
19
|
+
await purgeOldRows(); // runs every day at 03:00 (local time)
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
cron.on("error", (err, name) => console.warn(name, err));
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Durable scheduled work (compose with the queue)
|
|
26
|
+
|
|
27
|
+
A cron handler runs in-process, so for durable work, have it **enqueue** a job
|
|
28
|
+
into [`@monlite/queue`](https://www.npmjs.com/package/@monlite/queue) — the
|
|
29
|
+
schedule is persisted and the work is durable & retried:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createQueue } from "@monlite/queue";
|
|
33
|
+
const queue = createQueue(db);
|
|
34
|
+
queue.process("report", async (job) => generateReport(job.payload));
|
|
35
|
+
|
|
36
|
+
cron.schedule("nightly-report", "0 0 * * *", () => {
|
|
37
|
+
queue.add("report", { day: new Date().toISOString() });
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Cron syntax
|
|
42
|
+
|
|
43
|
+
Standard 5 fields — `minute hour day-of-month month day-of-week`:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
* every value 0 9 * * 1 09:00 every Monday
|
|
47
|
+
*/15 every 15 */15 * * * * every 15 minutes
|
|
48
|
+
1-5 range 0 9-17 * * * hourly, 9am–5pm
|
|
49
|
+
1,15 list 0 0 1,15 * * 1st and 15th at midnight
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Day-of-week is `0–6` (0 = Sunday). Times are **local**. When both day-of-month
|
|
53
|
+
and day-of-week are restricted, either match fires (POSIX behavior).
|
|
54
|
+
|
|
55
|
+
## API
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const cron = createCron(db, { checkInterval: 1000 }); // poll cadence (ms)
|
|
59
|
+
cron.schedule(name, cronExpr, handler); // register/update + start
|
|
60
|
+
cron.unschedule(name); // remove
|
|
61
|
+
cron.next(name); // next run (epoch ms) | undefined
|
|
62
|
+
cron.on("error", (err, name) => {});
|
|
63
|
+
cron.stop(); // stop scheduling (schedules persist)
|
|
64
|
+
|
|
65
|
+
// utility:
|
|
66
|
+
import { nextCronRun } from "@monlite/cron";
|
|
67
|
+
nextCronRun("0 9 * * 1"); // → Date of the next Monday 09:00
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Firing is **atomic** (a claim on the schedule row), so running the same schedule
|
|
71
|
+
in multiple processes fires each occurrence exactly once.
|
|
72
|
+
|
|
73
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var events = require('events');
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
function parseField(field, min, max) {
|
|
7
|
+
const out = /* @__PURE__ */ new Set();
|
|
8
|
+
for (const part of field.split(",")) {
|
|
9
|
+
const [rangePart, stepPart] = part.split("/");
|
|
10
|
+
const step = stepPart === void 0 ? 1 : parseInt(stepPart, 10);
|
|
11
|
+
let lo;
|
|
12
|
+
let hi;
|
|
13
|
+
if (rangePart === "*") {
|
|
14
|
+
lo = min;
|
|
15
|
+
hi = max;
|
|
16
|
+
} else if (rangePart.includes("-")) {
|
|
17
|
+
const [a, b] = rangePart.split("-");
|
|
18
|
+
lo = parseInt(a, 10);
|
|
19
|
+
hi = parseInt(b, 10);
|
|
20
|
+
} else {
|
|
21
|
+
lo = hi = parseInt(rangePart, 10);
|
|
22
|
+
}
|
|
23
|
+
if (Number.isNaN(lo) || Number.isNaN(hi) || Number.isNaN(step) || step < 1 || lo < min || hi > max || lo > hi) {
|
|
24
|
+
throw new Error(`Invalid cron field "${field}" (expected ${min}-${max})`);
|
|
25
|
+
}
|
|
26
|
+
for (let v = lo; v <= hi; v += step) out.add(v);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
function parseCron(expr) {
|
|
31
|
+
const parts = expr.trim().split(/\s+/);
|
|
32
|
+
if (parts.length !== 5) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Cron expression must have 5 fields, got ${parts.length}: "${expr}"`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
minute: parseField(parts[0], 0, 59),
|
|
39
|
+
hour: parseField(parts[1], 0, 23),
|
|
40
|
+
dom: parseField(parts[2], 1, 31),
|
|
41
|
+
month: parseField(parts[3], 1, 12),
|
|
42
|
+
dow: parseField(parts[4], 0, 6),
|
|
43
|
+
domRestricted: parts[2] !== "*",
|
|
44
|
+
dowRestricted: parts[4] !== "*"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function dayMatches(c, d) {
|
|
48
|
+
const dom = c.dom.has(d.getDate());
|
|
49
|
+
const dow = c.dow.has(d.getDay());
|
|
50
|
+
if (c.domRestricted && c.dowRestricted) return dom || dow;
|
|
51
|
+
return dom && dow;
|
|
52
|
+
}
|
|
53
|
+
function nextCronRun(expr, from = /* @__PURE__ */ new Date()) {
|
|
54
|
+
const c = typeof expr === "string" ? parseCron(expr) : expr;
|
|
55
|
+
const d = new Date(from.getTime());
|
|
56
|
+
d.setSeconds(0, 0);
|
|
57
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
58
|
+
for (let i = 0; i < 366 * 24 * 60 + 60; i++) {
|
|
59
|
+
if (c.minute.has(d.getMinutes()) && c.hour.has(d.getHours()) && c.month.has(d.getMonth() + 1) && dayMatches(c, d)) {
|
|
60
|
+
return d;
|
|
61
|
+
}
|
|
62
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`Could not compute next run for cron "${expr}"`);
|
|
65
|
+
}
|
|
66
|
+
var ensured = /* @__PURE__ */ new WeakSet();
|
|
67
|
+
var nowMs = () => Date.now();
|
|
68
|
+
var Cron = class extends events.EventEmitter {
|
|
69
|
+
driver;
|
|
70
|
+
checkInterval;
|
|
71
|
+
handlers = /* @__PURE__ */ new Map();
|
|
72
|
+
timer;
|
|
73
|
+
constructor(db, opts = {}) {
|
|
74
|
+
super();
|
|
75
|
+
this.driver = db.driver;
|
|
76
|
+
this.checkInterval = opts.checkInterval ?? 1e3;
|
|
77
|
+
if (!ensured.has(db)) {
|
|
78
|
+
this.driver.exec(
|
|
79
|
+
`CREATE TABLE IF NOT EXISTS _schedules (
|
|
80
|
+
name TEXT PRIMARY KEY, cron TEXT NOT NULL,
|
|
81
|
+
next_run INTEGER NOT NULL, last_run INTEGER
|
|
82
|
+
)`
|
|
83
|
+
);
|
|
84
|
+
ensured.add(db);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Register (or update) a schedule and start the scheduler. */
|
|
88
|
+
schedule(name, expr, handler) {
|
|
89
|
+
const c = parseCron(expr);
|
|
90
|
+
const existing = this.driver.prepare(`SELECT next_run FROM _schedules WHERE name = ?`).get(name);
|
|
91
|
+
const next = existing ? existing.next_run : nextCronRun(c).getTime();
|
|
92
|
+
this.driver.prepare(
|
|
93
|
+
`INSERT INTO _schedules (name, cron, next_run, last_run) VALUES (?, ?, ?, NULL)
|
|
94
|
+
ON CONFLICT(name) DO UPDATE SET cron = excluded.cron`
|
|
95
|
+
).run(name, expr, next);
|
|
96
|
+
this.handlers.set(name, { c, fn: handler });
|
|
97
|
+
if (!this.timer) {
|
|
98
|
+
this.timer = setInterval(() => this.tick(), this.checkInterval);
|
|
99
|
+
this.timer.unref?.();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Remove a schedule. */
|
|
103
|
+
unschedule(name) {
|
|
104
|
+
this.handlers.delete(name);
|
|
105
|
+
this.driver.prepare(`DELETE FROM _schedules WHERE name = ?`).run(name);
|
|
106
|
+
}
|
|
107
|
+
/** The next scheduled run (epoch ms) for a registered schedule. */
|
|
108
|
+
next(name) {
|
|
109
|
+
const row = this.driver.prepare(`SELECT next_run FROM _schedules WHERE name = ?`).get(name);
|
|
110
|
+
return row?.next_run;
|
|
111
|
+
}
|
|
112
|
+
/** @internal — exposed for tests; runs one scheduling pass. */
|
|
113
|
+
tick() {
|
|
114
|
+
const t = nowMs();
|
|
115
|
+
for (const [name, reg] of this.handlers) {
|
|
116
|
+
const row = this.driver.prepare(`SELECT next_run FROM _schedules WHERE name = ?`).get(name);
|
|
117
|
+
if (!row || row.next_run > t) continue;
|
|
118
|
+
const next = nextCronRun(reg.c, new Date(t)).getTime();
|
|
119
|
+
const claimed = this.driver.prepare(
|
|
120
|
+
`UPDATE _schedules SET last_run = ?, next_run = ? WHERE name = ? AND next_run <= ?`
|
|
121
|
+
).run(t, next, name, t).changes > 0;
|
|
122
|
+
if (claimed) {
|
|
123
|
+
Promise.resolve().then(() => reg.fn()).catch((err) => this.emit("error", err, name));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Stop the scheduler (schedules remain persisted). */
|
|
128
|
+
stop() {
|
|
129
|
+
if (this.timer) clearInterval(this.timer);
|
|
130
|
+
this.timer = void 0;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
function createCron(db, opts) {
|
|
134
|
+
return new Cron(db, opts);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
exports.Cron = Cron;
|
|
138
|
+
exports.createCron = createCron;
|
|
139
|
+
exports.nextCronRun = nextCronRun;
|
|
140
|
+
exports.parseCron = parseCron;
|
|
141
|
+
//# sourceMappingURL=index.cjs.map
|
|
142
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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 lo = hi = parseInt(rangePart, 10);\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 for (let i = 0; i < 366 * 24 * 60 + 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 FROM _schedules WHERE name = ?`)\n .get(name) as { next_run: number } | undefined;\n const next = existing ? existing.next_run : 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`,\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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { Monlite } from '@monlite/core';
|
|
3
|
+
|
|
4
|
+
interface CronOptions {
|
|
5
|
+
/** How often the scheduler checks for due jobs (ms). Default 1000. */
|
|
6
|
+
checkInterval?: number;
|
|
7
|
+
}
|
|
8
|
+
type CronHandler = () => void | Promise<void>;
|
|
9
|
+
interface ParsedCron {
|
|
10
|
+
minute: Set<number>;
|
|
11
|
+
hour: Set<number>;
|
|
12
|
+
dom: Set<number>;
|
|
13
|
+
month: Set<number>;
|
|
14
|
+
dow: Set<number>;
|
|
15
|
+
domRestricted: boolean;
|
|
16
|
+
dowRestricted: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Parse a standard 5-field cron expression (`min hour dom month dow`). */
|
|
19
|
+
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;
|
|
22
|
+
/**
|
|
23
|
+
* A persisted cron scheduler. Schedules survive restarts (next-run is stored),
|
|
24
|
+
* and firing is atomic so multiple processes won't double-run an occurrence.
|
|
25
|
+
* Compose with a queue for durable work: `cron.schedule(n, expr, () => queue.add(...))`.
|
|
26
|
+
* Emits `"error"` (err, name) if a handler throws.
|
|
27
|
+
*/
|
|
28
|
+
declare class Cron extends EventEmitter {
|
|
29
|
+
private readonly driver;
|
|
30
|
+
private readonly checkInterval;
|
|
31
|
+
private readonly handlers;
|
|
32
|
+
private timer;
|
|
33
|
+
constructor(db: Monlite, opts?: CronOptions);
|
|
34
|
+
/** Register (or update) a schedule and start the scheduler. */
|
|
35
|
+
schedule(name: string, expr: string, handler: CronHandler): void;
|
|
36
|
+
/** Remove a schedule. */
|
|
37
|
+
unschedule(name: string): void;
|
|
38
|
+
/** The next scheduled run (epoch ms) for a registered schedule. */
|
|
39
|
+
next(name: string): number | undefined;
|
|
40
|
+
/** @internal — exposed for tests; runs one scheduling pass. */
|
|
41
|
+
tick(): void;
|
|
42
|
+
/** Stop the scheduler (schedules remain persisted). */
|
|
43
|
+
stop(): void;
|
|
44
|
+
}
|
|
45
|
+
/** Create a cron scheduler over a monlite database. */
|
|
46
|
+
declare function createCron(db: Monlite, opts?: CronOptions): Cron;
|
|
47
|
+
|
|
48
|
+
export { Cron, type CronHandler, type CronOptions, type ParsedCron, createCron, nextCronRun, parseCron };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { Monlite } from '@monlite/core';
|
|
3
|
+
|
|
4
|
+
interface CronOptions {
|
|
5
|
+
/** How often the scheduler checks for due jobs (ms). Default 1000. */
|
|
6
|
+
checkInterval?: number;
|
|
7
|
+
}
|
|
8
|
+
type CronHandler = () => void | Promise<void>;
|
|
9
|
+
interface ParsedCron {
|
|
10
|
+
minute: Set<number>;
|
|
11
|
+
hour: Set<number>;
|
|
12
|
+
dom: Set<number>;
|
|
13
|
+
month: Set<number>;
|
|
14
|
+
dow: Set<number>;
|
|
15
|
+
domRestricted: boolean;
|
|
16
|
+
dowRestricted: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Parse a standard 5-field cron expression (`min hour dom month dow`). */
|
|
19
|
+
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;
|
|
22
|
+
/**
|
|
23
|
+
* A persisted cron scheduler. Schedules survive restarts (next-run is stored),
|
|
24
|
+
* and firing is atomic so multiple processes won't double-run an occurrence.
|
|
25
|
+
* Compose with a queue for durable work: `cron.schedule(n, expr, () => queue.add(...))`.
|
|
26
|
+
* Emits `"error"` (err, name) if a handler throws.
|
|
27
|
+
*/
|
|
28
|
+
declare class Cron extends EventEmitter {
|
|
29
|
+
private readonly driver;
|
|
30
|
+
private readonly checkInterval;
|
|
31
|
+
private readonly handlers;
|
|
32
|
+
private timer;
|
|
33
|
+
constructor(db: Monlite, opts?: CronOptions);
|
|
34
|
+
/** Register (or update) a schedule and start the scheduler. */
|
|
35
|
+
schedule(name: string, expr: string, handler: CronHandler): void;
|
|
36
|
+
/** Remove a schedule. */
|
|
37
|
+
unschedule(name: string): void;
|
|
38
|
+
/** The next scheduled run (epoch ms) for a registered schedule. */
|
|
39
|
+
next(name: string): number | undefined;
|
|
40
|
+
/** @internal — exposed for tests; runs one scheduling pass. */
|
|
41
|
+
tick(): void;
|
|
42
|
+
/** Stop the scheduler (schedules remain persisted). */
|
|
43
|
+
stop(): void;
|
|
44
|
+
}
|
|
45
|
+
/** Create a cron scheduler over a monlite database. */
|
|
46
|
+
declare function createCron(db: Monlite, opts?: CronOptions): Cron;
|
|
47
|
+
|
|
48
|
+
export { Cron, type CronHandler, type CronOptions, type ParsedCron, createCron, nextCronRun, parseCron };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
function parseField(field, min, max) {
|
|
5
|
+
const out = /* @__PURE__ */ new Set();
|
|
6
|
+
for (const part of field.split(",")) {
|
|
7
|
+
const [rangePart, stepPart] = part.split("/");
|
|
8
|
+
const step = stepPart === void 0 ? 1 : parseInt(stepPart, 10);
|
|
9
|
+
let lo;
|
|
10
|
+
let hi;
|
|
11
|
+
if (rangePart === "*") {
|
|
12
|
+
lo = min;
|
|
13
|
+
hi = max;
|
|
14
|
+
} else if (rangePart.includes("-")) {
|
|
15
|
+
const [a, b] = rangePart.split("-");
|
|
16
|
+
lo = parseInt(a, 10);
|
|
17
|
+
hi = parseInt(b, 10);
|
|
18
|
+
} else {
|
|
19
|
+
lo = hi = parseInt(rangePart, 10);
|
|
20
|
+
}
|
|
21
|
+
if (Number.isNaN(lo) || Number.isNaN(hi) || Number.isNaN(step) || step < 1 || lo < min || hi > max || lo > hi) {
|
|
22
|
+
throw new Error(`Invalid cron field "${field}" (expected ${min}-${max})`);
|
|
23
|
+
}
|
|
24
|
+
for (let v = lo; v <= hi; v += step) out.add(v);
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
function parseCron(expr) {
|
|
29
|
+
const parts = expr.trim().split(/\s+/);
|
|
30
|
+
if (parts.length !== 5) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Cron expression must have 5 fields, got ${parts.length}: "${expr}"`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
minute: parseField(parts[0], 0, 59),
|
|
37
|
+
hour: parseField(parts[1], 0, 23),
|
|
38
|
+
dom: parseField(parts[2], 1, 31),
|
|
39
|
+
month: parseField(parts[3], 1, 12),
|
|
40
|
+
dow: parseField(parts[4], 0, 6),
|
|
41
|
+
domRestricted: parts[2] !== "*",
|
|
42
|
+
dowRestricted: parts[4] !== "*"
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function dayMatches(c, d) {
|
|
46
|
+
const dom = c.dom.has(d.getDate());
|
|
47
|
+
const dow = c.dow.has(d.getDay());
|
|
48
|
+
if (c.domRestricted && c.dowRestricted) return dom || dow;
|
|
49
|
+
return dom && dow;
|
|
50
|
+
}
|
|
51
|
+
function nextCronRun(expr, from = /* @__PURE__ */ new Date()) {
|
|
52
|
+
const c = typeof expr === "string" ? parseCron(expr) : expr;
|
|
53
|
+
const d = new Date(from.getTime());
|
|
54
|
+
d.setSeconds(0, 0);
|
|
55
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
56
|
+
for (let i = 0; i < 366 * 24 * 60 + 60; i++) {
|
|
57
|
+
if (c.minute.has(d.getMinutes()) && c.hour.has(d.getHours()) && c.month.has(d.getMonth() + 1) && dayMatches(c, d)) {
|
|
58
|
+
return d;
|
|
59
|
+
}
|
|
60
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Could not compute next run for cron "${expr}"`);
|
|
63
|
+
}
|
|
64
|
+
var ensured = /* @__PURE__ */ new WeakSet();
|
|
65
|
+
var nowMs = () => Date.now();
|
|
66
|
+
var Cron = class extends EventEmitter {
|
|
67
|
+
driver;
|
|
68
|
+
checkInterval;
|
|
69
|
+
handlers = /* @__PURE__ */ new Map();
|
|
70
|
+
timer;
|
|
71
|
+
constructor(db, opts = {}) {
|
|
72
|
+
super();
|
|
73
|
+
this.driver = db.driver;
|
|
74
|
+
this.checkInterval = opts.checkInterval ?? 1e3;
|
|
75
|
+
if (!ensured.has(db)) {
|
|
76
|
+
this.driver.exec(
|
|
77
|
+
`CREATE TABLE IF NOT EXISTS _schedules (
|
|
78
|
+
name TEXT PRIMARY KEY, cron TEXT NOT NULL,
|
|
79
|
+
next_run INTEGER NOT NULL, last_run INTEGER
|
|
80
|
+
)`
|
|
81
|
+
);
|
|
82
|
+
ensured.add(db);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Register (or update) a schedule and start the scheduler. */
|
|
86
|
+
schedule(name, expr, handler) {
|
|
87
|
+
const c = parseCron(expr);
|
|
88
|
+
const existing = this.driver.prepare(`SELECT next_run FROM _schedules WHERE name = ?`).get(name);
|
|
89
|
+
const next = existing ? existing.next_run : nextCronRun(c).getTime();
|
|
90
|
+
this.driver.prepare(
|
|
91
|
+
`INSERT INTO _schedules (name, cron, next_run, last_run) VALUES (?, ?, ?, NULL)
|
|
92
|
+
ON CONFLICT(name) DO UPDATE SET cron = excluded.cron`
|
|
93
|
+
).run(name, expr, next);
|
|
94
|
+
this.handlers.set(name, { c, fn: handler });
|
|
95
|
+
if (!this.timer) {
|
|
96
|
+
this.timer = setInterval(() => this.tick(), this.checkInterval);
|
|
97
|
+
this.timer.unref?.();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Remove a schedule. */
|
|
101
|
+
unschedule(name) {
|
|
102
|
+
this.handlers.delete(name);
|
|
103
|
+
this.driver.prepare(`DELETE FROM _schedules WHERE name = ?`).run(name);
|
|
104
|
+
}
|
|
105
|
+
/** The next scheduled run (epoch ms) for a registered schedule. */
|
|
106
|
+
next(name) {
|
|
107
|
+
const row = this.driver.prepare(`SELECT next_run FROM _schedules WHERE name = ?`).get(name);
|
|
108
|
+
return row?.next_run;
|
|
109
|
+
}
|
|
110
|
+
/** @internal — exposed for tests; runs one scheduling pass. */
|
|
111
|
+
tick() {
|
|
112
|
+
const t = nowMs();
|
|
113
|
+
for (const [name, reg] of this.handlers) {
|
|
114
|
+
const row = this.driver.prepare(`SELECT next_run FROM _schedules WHERE name = ?`).get(name);
|
|
115
|
+
if (!row || row.next_run > t) continue;
|
|
116
|
+
const next = nextCronRun(reg.c, new Date(t)).getTime();
|
|
117
|
+
const claimed = this.driver.prepare(
|
|
118
|
+
`UPDATE _schedules SET last_run = ?, next_run = ? WHERE name = ? AND next_run <= ?`
|
|
119
|
+
).run(t, next, name, t).changes > 0;
|
|
120
|
+
if (claimed) {
|
|
121
|
+
Promise.resolve().then(() => reg.fn()).catch((err) => this.emit("error", err, name));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Stop the scheduler (schedules remain persisted). */
|
|
126
|
+
stop() {
|
|
127
|
+
if (this.timer) clearInterval(this.timer);
|
|
128
|
+
this.timer = void 0;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
function createCron(db, opts) {
|
|
132
|
+
return new Cron(db, opts);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { Cron, createCron, nextCronRun, parseCron };
|
|
136
|
+
//# sourceMappingURL=index.js.map
|
|
137
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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 lo = hi = parseInt(rangePart, 10);\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 for (let i = 0; i < 366 * 24 * 60 + 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 FROM _schedules WHERE name = ?`)\n .get(name) as { next_run: number } | undefined;\n const next = existing ? existing.next_run : 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`,\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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@monlite/cron",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cron-style job scheduling for @monlite/core: persisted schedules, 5-field cron, multi-process safe.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"keywords": [
|
|
26
|
+
"monlite",
|
|
27
|
+
"cron",
|
|
28
|
+
"scheduler",
|
|
29
|
+
"schedule",
|
|
30
|
+
"jobs",
|
|
31
|
+
"tasks",
|
|
32
|
+
"sqlite"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"author": "Emad Jumaah <emadjumaah@gmail.com>",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/qataruts/monlite.git",
|
|
39
|
+
"directory": "packages/kv"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/qataruts/monlite/tree/main/packages/kv#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/qataruts/monlite/issues"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@monlite/core": "^1.0.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^22.10.0",
|
|
56
|
+
"better-sqlite3": "^11.8.0",
|
|
57
|
+
"tsup": "^8.3.5",
|
|
58
|
+
"typescript": "^5.7.2",
|
|
59
|
+
"vitest": "^2.1.8"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "tsup",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"typecheck": "tsc --noEmit"
|
|
65
|
+
}
|
|
66
|
+
}
|