@monlite/cron 0.1.2 → 0.1.3

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
@@ -68,12 +68,14 @@ var ensured = /* @__PURE__ */ new WeakSet();
68
68
  var nowMs = () => Date.now();
69
69
  var Cron = class extends events.EventEmitter {
70
70
  driver;
71
+ heartbeat;
71
72
  checkInterval;
72
73
  handlers = /* @__PURE__ */ new Map();
73
- timer;
74
+ task;
74
75
  constructor(db, opts = {}) {
75
76
  super();
76
77
  this.driver = db.driver;
78
+ this.heartbeat = db.heartbeat;
77
79
  this.checkInterval = opts.checkInterval ?? 1e3;
78
80
  if (!ensured.has(db)) {
79
81
  this.driver.exec(
@@ -95,9 +97,8 @@ var Cron = class extends events.EventEmitter {
95
97
  ON CONFLICT(name) DO UPDATE SET cron = excluded.cron, next_run = excluded.next_run`
96
98
  ).run(name, expr, next);
97
99
  this.handlers.set(name, { c, fn: handler });
98
- if (!this.timer) {
99
- this.timer = setInterval(() => this.tick(), this.checkInterval);
100
- this.timer.unref?.();
100
+ if (!this.task) {
101
+ this.task = this.heartbeat.every(this.checkInterval, () => this.tick());
101
102
  }
102
103
  }
103
104
  /** Remove a schedule. */
@@ -127,8 +128,8 @@ var Cron = class extends events.EventEmitter {
127
128
  }
128
129
  /** Stop the scheduler (schedules remain persisted). */
129
130
  stop() {
130
- if (this.timer) clearInterval(this.timer);
131
- this.timer = void 0;
131
+ if (this.task) this.task.cancel();
132
+ this.task = void 0;
132
133
  }
133
134
  };
134
135
  function createCron(db, opts) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["EventEmitter"],"mappings":";;;;;AAoBA,SAAS,UAAA,CAAW,KAAA,EAAe,GAAA,EAAa,GAAA,EAA0B;AACxE,EAAA,MAAM,GAAA,uBAAU,GAAA,EAAY;AAC5B,EAAA,KAAA,MAAW,IAAA,IAAQ,KAAA,CAAM,KAAA,CAAM,GAAG,CAAA,EAAG;AACnC,IAAA,MAAM,CAAC,SAAA,EAAW,QAAQ,CAAA,GAAI,IAAA,CAAK,MAAM,GAAG,CAAA;AAC5C,IAAA,MAAM,OAAO,QAAA,KAAa,MAAA,GAAY,CAAA,GAAI,QAAA,CAAS,UAAU,EAAE,CAAA;AAC/D,IAAA,IAAI,EAAA;AACJ,IAAA,IAAI,EAAA;AACJ,IAAA,IAAI,cAAc,GAAA,EAAK;AACrB,MAAA,EAAA,GAAK,GAAA;AACL,MAAA,EAAA,GAAK,GAAA;AAAA,IACP,CAAA,MAAA,IAAW,SAAA,CAAU,QAAA,CAAS,GAAG,CAAA,EAAG;AAClC,MAAA,MAAM,CAAC,CAAA,EAAG,CAAC,CAAA,GAAI,SAAA,CAAU,MAAM,GAAG,CAAA;AAClC,MAAA,EAAA,GAAK,QAAA,CAAS,GAAG,EAAE,CAAA;AACnB,MAAA,EAAA,GAAK,QAAA,CAAS,GAAG,EAAE,CAAA;AAAA,IACrB,CAAA,MAAO;AAGL,MAAA,EAAA,GAAK,QAAA,CAAS,WAAW,EAAE,CAAA;AAC3B,MAAA,EAAA,GAAK,QAAA,KAAa,SAAY,EAAA,GAAK,GAAA;AAAA,IACrC;AACA,IAAA,IACE,OAAO,KAAA,CAAM,EAAE,KACf,MAAA,CAAO,KAAA,CAAM,EAAE,CAAA,IACf,MAAA,CAAO,MAAM,IAAI,CAAA,IACjB,OAAO,CAAA,IACP,EAAA,GAAK,OACL,EAAA,GAAK,GAAA,IACL,KAAK,EAAA,EACL;AACA,MAAA,MAAM,IAAI,MAAM,CAAA,oBAAA,EAAuB,KAAK,eAAe,GAAG,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA,CAAG,CAAA;AAAA,IAC1E;AACA,IAAA,KAAA,IAAS,CAAA,GAAI,IAAI,CAAA,IAAK,EAAA,EAAI,KAAK,IAAA,EAAM,GAAA,CAAI,IAAI,CAAC,CAAA;AAAA,EAChD;AACA,EAAA,OAAO,GAAA;AACT;AAGO,SAAS,UAAU,IAAA,EAA0B;AAClD,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,IAAA,EAAK,CAAE,MAAM,KAAK,CAAA;AACrC,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,wCAAA,EAA2C,KAAA,CAAM,MAAM,CAAA,GAAA,EAAM,IAAI,CAAA,CAAA;AAAA,KACnE;AAAA,EACF;AACA,EAAA,OAAO;AAAA,IACL,QAAQ,UAAA,CAAW,KAAA,CAAM,CAAC,CAAA,EAAG,GAAG,EAAE,CAAA;AAAA,IAClC,MAAM,UAAA,CAAW,KAAA,CAAM,CAAC,CAAA,EAAG,GAAG,EAAE,CAAA;AAAA,IAChC,KAAK,UAAA,CAAW,KAAA,CAAM,CAAC,CAAA,EAAG,GAAG,EAAE,CAAA;AAAA,IAC/B,OAAO,UAAA,CAAW,KAAA,CAAM,CAAC,CAAA,EAAG,GAAG,EAAE,CAAA;AAAA,IACjC,KAAK,UAAA,CAAW,KAAA,CAAM,CAAC,CAAA,EAAG,GAAG,CAAC,CAAA;AAAA,IAC9B,aAAA,EAAe,KAAA,CAAM,CAAC,CAAA,KAAM,GAAA;AAAA,IAC5B,aAAA,EAAe,KAAA,CAAM,CAAC,CAAA,KAAM;AAAA,GAC9B;AACF;AAEA,SAAS,UAAA,CAAW,GAAe,CAAA,EAAkB;AACnD,EAAA,MAAM,MAAM,CAAA,CAAE,GAAA,CAAI,GAAA,CAAI,CAAA,CAAE,SAAS,CAAA;AACjC,EAAA,MAAM,MAAM,CAAA,CAAE,GAAA,CAAI,GAAA,CAAI,CAAA,CAAE,QAAQ,CAAA;AAEhC,EAAA,IAAI,CAAA,CAAE,aAAA,IAAiB,CAAA,CAAE,aAAA,SAAsB,GAAA,IAAO,GAAA;AACtD,EAAA,OAAO,GAAA,IAAO,GAAA;AAChB;AAGO,SAAS,WAAA,CACd,IAAA,EACA,IAAA,mBAAa,IAAI,MAAK,EAChB;AACN,EAAA,MAAM,IAAI,OAAO,IAAA,KAAS,QAAA,GAAW,SAAA,CAAU,IAAI,CAAA,GAAI,IAAA;AACvD,EAAA,MAAM,CAAA,GAAI,IAAI,IAAA,CAAK,IAAA,CAAK,SAAS,CAAA;AACjC,EAAA,CAAA,CAAE,UAAA,CAAW,GAAG,CAAC,CAAA;AACjB,EAAA,CAAA,CAAE,UAAA,CAAW,CAAA,CAAE,UAAA,EAAW,GAAI,CAAC,CAAA;AAI/B,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,IAAI,GAAA,GAAM,EAAA,GAAK,IAAI,CAAA,EAAA,EAAK;AAC1C,IAAA,IACE,CAAA,CAAE,MAAA,CAAO,GAAA,CAAI,CAAA,CAAE,UAAA,EAAY,CAAA,IAC3B,CAAA,CAAE,IAAA,CAAK,GAAA,CAAI,CAAA,CAAE,QAAA,EAAU,CAAA,IACvB,CAAA,CAAE,KAAA,CAAM,GAAA,CAAI,CAAA,CAAE,QAAA,EAAS,GAAI,CAAC,CAAA,IAC5B,UAAA,CAAW,CAAA,EAAG,CAAC,CAAA,EACf;AACA,MAAA,OAAO,CAAA;AAAA,IACT;AACA,IAAA,CAAA,CAAE,UAAA,CAAW,CAAA,CAAE,UAAA,EAAW,GAAI,CAAC,CAAA;AAAA,EACjC;AACA,EAAA,MAAM,IAAI,KAAA,CAAM,CAAA,qCAAA,EAAwC,IAAI,CAAA,CAAA,CAAG,CAAA;AACjE;AAEA,IAAM,OAAA,uBAAc,OAAA,EAAgB;AACpC,IAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,GAAA,EAAI;AAQtB,IAAM,IAAA,GAAN,cAAmBA,mBAAA,CAAa;AAAA,EACpB,MAAA;AAAA,EACA,aAAA;AAAA,EACA,QAAA,uBAAe,GAAA,EAG9B;AAAA,EACM,KAAA;AAAA,EAER,WAAA,CAAY,EAAA,EAAa,IAAA,GAAoB,EAAC,EAAG;AAC/C,IAAA,KAAA,EAAM;AACN,IAAA,IAAA,CAAK,SAAS,EAAA,CAAG,MAAA;AACjB,IAAA,IAAA,CAAK,aAAA,GAAgB,KAAK,aAAA,IAAiB,GAAA;AAC3C,IAAA,IAAI,CAAC,OAAA,CAAQ,GAAA,CAAI,EAAE,CAAA,EAAG;AACpB,MAAA,IAAA,CAAK,MAAA,CAAO,IAAA;AAAA,QACV,CAAA;AAAA;AAAA;AAAA,SAAA;AAAA,OAIF;AACA,MAAA,OAAA,CAAQ,IAAI,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA,EAGA,QAAA,CAAS,IAAA,EAAc,IAAA,EAAc,OAAA,EAA4B;AAC/D,IAAA,MAAM,CAAA,GAAI,UAAU,IAAI,CAAA;AACxB,IAAA,MAAM,WAAW,IAAA,CAAK,MAAA,CACnB,QAAQ,CAAA,oDAAA,CAAsD,CAAA,CAC9D,IAAI,IAAI,CAAA;AAIX,IAAA,MAAM,IAAA,GACJ,QAAA,IAAY,QAAA,CAAS,IAAA,KAAS,IAAA,GAC1B,SAAS,QAAA,GACT,WAAA,CAAY,CAAC,CAAA,CAAE,OAAA,EAAQ;AAC7B,IAAA,IAAA,CAAK,MAAA,CACF,OAAA;AAAA,MACC,CAAA;AAAA,2FAAA;AAAA,KAEF,CACC,GAAA,CAAI,IAAA,EAAM,IAAA,EAAM,IAAI,CAAA;AACvB,IAAA,IAAA,CAAK,SAAS,GAAA,CAAI,IAAA,EAAM,EAAE,CAAA,EAAG,EAAA,EAAI,SAAS,CAAA;AAC1C,IAAA,IAAI,CAAC,KAAK,KAAA,EAAO;AACf,MAAA,IAAA,CAAK,QAAQ,WAAA,CAAY,MAAM,KAAK,IAAA,EAAK,EAAG,KAAK,aAAa,CAAA;AAC9D,MAAA,IAAA,CAAK,MAAM,KAAA,IAAQ;AAAA,IACrB;AAAA,EACF;AAAA;AAAA,EAGA,WAAW,IAAA,EAAoB;AAC7B,IAAA,IAAA,CAAK,QAAA,CAAS,OAAO,IAAI,CAAA;AACzB,IAAA,IAAA,CAAK,MAAA,CAAO,OAAA,CAAQ,CAAA,qCAAA,CAAuC,CAAA,CAAE,IAAI,IAAI,CAAA;AAAA,EACvE;AAAA;AAAA,EAGA,KAAK,IAAA,EAAkC;AACrC,IAAA,MAAM,MAAM,IAAA,CAAK,MAAA,CACd,QAAQ,CAAA,8CAAA,CAAgD,CAAA,CACxD,IAAI,IAAI,CAAA;AACX,IAAA,OAAO,GAAA,EAAK,QAAA;AAAA,EACd;AAAA;AAAA,EAGA,IAAA,GAAa;AACX,IAAA,MAAM,IAAI,KAAA,EAAM;AAChB,IAAA,KAAA,MAAW,CAAC,IAAA,EAAM,GAAG,CAAA,IAAK,KAAK,QAAA,EAAU;AACvC,MAAA,MAAM,MAAM,IAAA,CAAK,MAAA,CACd,QAAQ,CAAA,8CAAA,CAAgD,CAAA,CACxD,IAAI,IAAI,CAAA;AACX,MAAA,IAAI,CAAC,GAAA,IAAO,GAAA,CAAI,QAAA,GAAW,CAAA,EAAG;AAC9B,MAAA,MAAM,IAAA,GAAO,YAAY,GAAA,CAAI,CAAA,EAAG,IAAI,IAAA,CAAK,CAAC,CAAC,CAAA,CAAE,OAAA,EAAQ;AAErD,MAAA,MAAM,OAAA,GACJ,KAAK,MAAA,CACF,OAAA;AAAA,QACC,CAAA,iFAAA;AAAA,QAED,GAAA,CAAI,CAAA,EAAG,MAAM,IAAA,EAAM,CAAC,EAAE,OAAA,GAAU,CAAA;AACrC,MAAA,IAAI,OAAA,EAAS;AACX,QAAA,OAAA,CAAQ,SAAQ,CACb,IAAA,CAAK,MAAM,GAAA,CAAI,IAAI,CAAA,CACnB,KAAA,CAAM,CAAC,QAAQ,IAAA,CAAK,IAAA,CAAK,OAAA,EAAS,GAAA,EAAK,IAAI,CAAC,CAAA;AAAA,MACjD;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,IAAA,GAAa;AACX,IAAA,IAAI,IAAA,CAAK,KAAA,EAAO,aAAA,CAAc,IAAA,CAAK,KAAK,CAAA;AACxC,IAAA,IAAA,CAAK,KAAA,GAAQ,MAAA;AAAA,EACf;AACF;AAGO,SAAS,UAAA,CAAW,IAAa,IAAA,EAA0B;AAChE,EAAA,OAAO,IAAI,IAAA,CAAK,EAAA,EAAI,IAAI,CAAA;AAC1B","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\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"]}
package/dist/index.d.cts CHANGED
@@ -27,9 +27,10 @@ declare function nextCronRun(expr: string | ParsedCron, from?: Date): Date;
27
27
  */
28
28
  declare class Cron extends EventEmitter {
29
29
  private readonly driver;
30
+ private readonly heartbeat;
30
31
  private readonly checkInterval;
31
32
  private readonly handlers;
32
- private timer;
33
+ private task;
33
34
  constructor(db: Monlite, opts?: CronOptions);
34
35
  /** Register (or update) a schedule and start the scheduler. */
35
36
  schedule(name: string, expr: string, handler: CronHandler): void;
package/dist/index.d.ts CHANGED
@@ -27,9 +27,10 @@ declare function nextCronRun(expr: string | ParsedCron, from?: Date): Date;
27
27
  */
28
28
  declare class Cron extends EventEmitter {
29
29
  private readonly driver;
30
+ private readonly heartbeat;
30
31
  private readonly checkInterval;
31
32
  private readonly handlers;
32
- private timer;
33
+ private task;
33
34
  constructor(db: Monlite, opts?: CronOptions);
34
35
  /** Register (or update) a schedule and start the scheduler. */
35
36
  schedule(name: string, expr: string, handler: CronHandler): void;
package/dist/index.js CHANGED
@@ -66,12 +66,14 @@ var ensured = /* @__PURE__ */ new WeakSet();
66
66
  var nowMs = () => Date.now();
67
67
  var Cron = class extends EventEmitter {
68
68
  driver;
69
+ heartbeat;
69
70
  checkInterval;
70
71
  handlers = /* @__PURE__ */ new Map();
71
- timer;
72
+ task;
72
73
  constructor(db, opts = {}) {
73
74
  super();
74
75
  this.driver = db.driver;
76
+ this.heartbeat = db.heartbeat;
75
77
  this.checkInterval = opts.checkInterval ?? 1e3;
76
78
  if (!ensured.has(db)) {
77
79
  this.driver.exec(
@@ -93,9 +95,8 @@ var Cron = class extends EventEmitter {
93
95
  ON CONFLICT(name) DO UPDATE SET cron = excluded.cron, next_run = excluded.next_run`
94
96
  ).run(name, expr, next);
95
97
  this.handlers.set(name, { c, fn: handler });
96
- if (!this.timer) {
97
- this.timer = setInterval(() => this.tick(), this.checkInterval);
98
- this.timer.unref?.();
98
+ if (!this.task) {
99
+ this.task = this.heartbeat.every(this.checkInterval, () => this.tick());
99
100
  }
100
101
  }
101
102
  /** Remove a schedule. */
@@ -125,8 +126,8 @@ var Cron = class extends EventEmitter {
125
126
  }
126
127
  /** Stop the scheduler (schedules remain persisted). */
127
128
  stop() {
128
- if (this.timer) clearInterval(this.timer);
129
- this.timer = void 0;
129
+ if (this.task) this.task.cancel();
130
+ this.task = void 0;
130
131
  }
131
132
  };
132
133
  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\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monlite/cron",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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.6.13"
52
+ "@monlite/core": "^2.8.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/node": "^22.10.0",