@cronwatch/sdk 0.3.1 → 0.5.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.
Files changed (136) hide show
  1. package/README.md +8 -2
  2. package/dist/anthropic.d.cts +1 -1
  3. package/dist/anthropic.d.ts +1 -1
  4. package/dist/bugsnag.cjs +108 -0
  5. package/dist/bugsnag.cjs.map +1 -0
  6. package/dist/bugsnag.d.cts +26 -0
  7. package/dist/bugsnag.d.ts +26 -0
  8. package/dist/bugsnag.js +106 -0
  9. package/dist/bugsnag.js.map +1 -0
  10. package/dist/client-B7bqnkI5.d.cts +450 -0
  11. package/dist/client-pmqH3apS.d.ts +450 -0
  12. package/dist/d1.cjs +214 -0
  13. package/dist/d1.cjs.map +1 -0
  14. package/dist/d1.d.cts +42 -0
  15. package/dist/d1.d.ts +42 -0
  16. package/dist/d1.js +212 -0
  17. package/dist/d1.js.map +1 -0
  18. package/dist/datadog.cjs +95 -0
  19. package/dist/datadog.cjs.map +1 -0
  20. package/dist/datadog.d.cts +24 -0
  21. package/dist/datadog.d.ts +24 -0
  22. package/dist/datadog.js +93 -0
  23. package/dist/datadog.js.map +1 -0
  24. package/dist/discord.cjs +2 -0
  25. package/dist/discord.cjs.map +1 -1
  26. package/dist/discord.d.cts +1 -1
  27. package/dist/discord.d.ts +1 -1
  28. package/dist/discord.js +2 -0
  29. package/dist/discord.js.map +1 -1
  30. package/dist/email-BJ-z_epD.d.cts +21 -0
  31. package/dist/email-_H0faCh3.d.ts +21 -0
  32. package/dist/honeybadger.cjs +101 -0
  33. package/dist/honeybadger.cjs.map +1 -0
  34. package/dist/honeybadger.d.cts +23 -0
  35. package/dist/honeybadger.d.ts +23 -0
  36. package/dist/honeybadger.js +99 -0
  37. package/dist/honeybadger.js.map +1 -0
  38. package/dist/index.cjs +1238 -179
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +5 -260
  41. package/dist/index.d.ts +5 -260
  42. package/dist/index.js +1238 -179
  43. package/dist/index.js.map +1 -1
  44. package/dist/mailgun.cjs +115 -0
  45. package/dist/mailgun.cjs.map +1 -0
  46. package/dist/mailgun.d.cts +21 -0
  47. package/dist/mailgun.d.ts +21 -0
  48. package/dist/mailgun.js +113 -0
  49. package/dist/mailgun.js.map +1 -0
  50. package/dist/newrelic.cjs +88 -0
  51. package/dist/newrelic.cjs.map +1 -0
  52. package/dist/newrelic.d.cts +25 -0
  53. package/dist/newrelic.d.ts +25 -0
  54. package/dist/newrelic.js +86 -0
  55. package/dist/newrelic.js.map +1 -0
  56. package/dist/node.cjs +191 -0
  57. package/dist/node.cjs.map +1 -0
  58. package/dist/node.d.cts +97 -0
  59. package/dist/node.d.ts +97 -0
  60. package/dist/node.js +186 -0
  61. package/dist/node.js.map +1 -0
  62. package/dist/pg-cron.cjs +286 -0
  63. package/dist/pg-cron.cjs.map +1 -0
  64. package/dist/pg-cron.d.cts +93 -0
  65. package/dist/pg-cron.d.ts +93 -0
  66. package/dist/pg-cron.js +280 -0
  67. package/dist/pg-cron.js.map +1 -0
  68. package/dist/postgres.cjs +20 -0
  69. package/dist/postgres.cjs.map +1 -1
  70. package/dist/postgres.d.cts +1 -1
  71. package/dist/postgres.d.ts +1 -1
  72. package/dist/postgres.js +20 -0
  73. package/dist/postgres.js.map +1 -1
  74. package/dist/postmark.cjs +106 -0
  75. package/dist/postmark.cjs.map +1 -0
  76. package/dist/postmark.d.cts +18 -0
  77. package/dist/postmark.d.ts +18 -0
  78. package/dist/postmark.js +104 -0
  79. package/dist/postmark.js.map +1 -0
  80. package/dist/resend.cjs +114 -0
  81. package/dist/resend.cjs.map +1 -0
  82. package/dist/resend.d.cts +16 -0
  83. package/dist/resend.d.ts +16 -0
  84. package/dist/resend.js +112 -0
  85. package/dist/resend.js.map +1 -0
  86. package/dist/rollbar.cjs +108 -0
  87. package/dist/rollbar.cjs.map +1 -0
  88. package/dist/rollbar.d.cts +20 -0
  89. package/dist/rollbar.d.ts +20 -0
  90. package/dist/rollbar.js +106 -0
  91. package/dist/rollbar.js.map +1 -0
  92. package/dist/sendgrid.cjs +114 -0
  93. package/dist/sendgrid.cjs.map +1 -0
  94. package/dist/sendgrid.d.cts +19 -0
  95. package/dist/sendgrid.d.ts +19 -0
  96. package/dist/sendgrid.js +112 -0
  97. package/dist/sendgrid.js.map +1 -0
  98. package/dist/sentry.cjs +130 -0
  99. package/dist/sentry.cjs.map +1 -0
  100. package/dist/sentry.d.cts +28 -0
  101. package/dist/sentry.d.ts +28 -0
  102. package/dist/sentry.js +127 -0
  103. package/dist/sentry.js.map +1 -0
  104. package/dist/ses.cjs +170 -0
  105. package/dist/ses.cjs.map +1 -0
  106. package/dist/ses.d.cts +25 -0
  107. package/dist/ses.d.ts +25 -0
  108. package/dist/ses.js +168 -0
  109. package/dist/ses.js.map +1 -0
  110. package/dist/slack.cjs +2 -0
  111. package/dist/slack.cjs.map +1 -1
  112. package/dist/slack.d.cts +1 -1
  113. package/dist/slack.d.ts +1 -1
  114. package/dist/slack.js +2 -0
  115. package/dist/slack.js.map +1 -1
  116. package/dist/sqlite.cjs +18 -0
  117. package/dist/sqlite.cjs.map +1 -1
  118. package/dist/sqlite.d.cts +1 -1
  119. package/dist/sqlite.d.ts +1 -1
  120. package/dist/sqlite.js +18 -0
  121. package/dist/sqlite.js.map +1 -1
  122. package/dist/twilio.cjs +168 -0
  123. package/dist/twilio.cjs.map +1 -0
  124. package/dist/twilio.d.cts +52 -0
  125. package/dist/twilio.d.ts +52 -0
  126. package/dist/twilio.js +162 -0
  127. package/dist/twilio.js.map +1 -0
  128. package/dist/{types-DUt2apl0.d.cts → types-Ddq1MUPL.d.cts} +29 -2
  129. package/dist/{types-DUt2apl0.d.ts → types-Ddq1MUPL.d.ts} +29 -2
  130. package/dist/webhook.cjs +11 -5
  131. package/dist/webhook.cjs.map +1 -1
  132. package/dist/webhook.d.cts +6 -3
  133. package/dist/webhook.d.ts +6 -3
  134. package/dist/webhook.js +11 -6
  135. package/dist/webhook.js.map +1 -1
  136. package/package.json +208 -2
@@ -0,0 +1,280 @@
1
+ // src/sources/pgcron.ts
2
+ var BACKFILL = 20;
3
+ var PAGE = 500;
4
+ var MAX_PAGES = 10;
5
+ var PG_CRON_HOLD_MS = 10 * 6e4;
6
+ var JOBS_SQL = `SELECT jobid, jobname, schedule, database, username, active FROM cron.job ORDER BY jobid`;
7
+ var SETTING_SQL = `SELECT setting FROM pg_settings WHERE name = $1`;
8
+ var COLUMNS = `d.runid, d.jobid, d.status, d.return_message, d.start_time, d.end_time`;
9
+ var RUNS_SQL = `SELECT ${COLUMNS}
10
+ FROM cron.job_run_details d
11
+ LEFT JOIN unnest($1::bigint[], $2::bigint[]) AS c(jobid, after) ON d.jobid = c.jobid
12
+ WHERE d.runid > c.after OR d.runid = ANY($3::bigint[])
13
+ ORDER BY d.runid LIMIT ${PAGE}`;
14
+ var NEWEST_SQL = `SELECT ${COLUMNS} FROM cron.job_run_details d WHERE d.jobid = $1 ORDER BY d.runid DESC LIMIT ${BACKFILL}`;
15
+ var finishedStatus = (status) => status === "succeeded" || status === "failed";
16
+ function pgCronSchedule(schedule) {
17
+ const text = schedule.trim();
18
+ const seconds = /^(\d+)\s*seconds?$/i.exec(text);
19
+ if (seconds) return `every ${Number(seconds[1])}s`;
20
+ if (/^@reboot$/i.test(text)) return null;
21
+ const fields = text.split(/\s+/);
22
+ if (fields.length > 5 && !fields[0].startsWith("@")) fields.length = 5;
23
+ if (fields.length === 5 && fields[2].includes("$")) fields[2] = fields[2].replace(/\$/g, "L");
24
+ return fields.join(" ");
25
+ }
26
+ function pgCronJobName(job) {
27
+ const cleaned = (job.jobname ?? "").replace(/[^A-Za-z0-9._:-]+/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 100);
28
+ return cleaned || `pg_cron:${job.jobid}`;
29
+ }
30
+ function pgCronRun(row, job, idPrefix, fallbackAt = Date.now()) {
31
+ const finishedAt = row.end_time === null ? null : new Date(row.end_time).getTime();
32
+ const done = finishedStatus(row.status);
33
+ if (row.start_time === null && !done) return null;
34
+ const startedAt = row.start_time !== null ? new Date(row.start_time).getTime() : finishedAt ?? fallbackAt;
35
+ const message = row.return_message === null ? null : row.return_message.trim() || null;
36
+ const status = row.status === "succeeded" ? "ok" : row.status === "failed" ? "failed" : "running";
37
+ const end = done ? Math.max(startedAt, finishedAt ?? startedAt) : null;
38
+ return {
39
+ id: `${idPrefix}${row.runid}`,
40
+ job,
41
+ status,
42
+ startedAt,
43
+ finishedAt: end,
44
+ durationMs: end === null ? null : end - startedAt,
45
+ error: status === "failed" ? message ?? "pg_cron reported the run as failed" : null,
46
+ output: status === "ok" ? message : null,
47
+ metrics: {},
48
+ trigger: "pg_cron"
49
+ };
50
+ }
51
+ function unscheduled(definition) {
52
+ const { description, tags, grace, timeout, maxDuration, budget, failuresBeforeAlert } = definition;
53
+ const out = {};
54
+ if (description !== void 0) out.description = description;
55
+ if (tags !== void 0) out.tags = tags;
56
+ if (grace !== void 0) out.grace = grace;
57
+ if (timeout !== void 0) out.timeout = timeout;
58
+ if (maxDuration !== void 0) out.maxDuration = maxDuration;
59
+ if (budget !== void 0) out.budget = budget;
60
+ if (failuresBeforeAlert !== void 0) out.failuresBeforeAlert = failuresBeforeAlert;
61
+ return out;
62
+ }
63
+ var keyOf = (definition) => JSON.stringify(definition, (_k, v) => v instanceof RegExp || typeof v === "function" ? String(v) : v);
64
+ function pgCron(db, options = {}) {
65
+ const prefix = options.prefix ?? "";
66
+ const idPrefix = `pgcron:${prefix}`;
67
+ const cursors = /* @__PURE__ */ new Map();
68
+ const lastAt = /* @__PURE__ */ new Map();
69
+ const pending = /* @__PURE__ */ new Map();
70
+ const held = /* @__PURE__ */ new Map();
71
+ let known = /* @__PURE__ */ new Map();
72
+ const declared = /* @__PURE__ */ new Map();
73
+ const retired = /* @__PURE__ */ new Set();
74
+ let scanned = false;
75
+ const warned = /* @__PURE__ */ new Set();
76
+ const warnOnce = (host, key, message) => {
77
+ if (warned.has(key)) return;
78
+ warned.add(key);
79
+ host.onError(new Error(message), "source pg_cron");
80
+ };
81
+ const picks = (job) => {
82
+ const { jobs } = options;
83
+ if (jobs === void 0) return true;
84
+ if (typeof jobs === "function") return jobs(job);
85
+ return jobs.some((j) => typeof j === "number" ? j === job.jobid : j === job.jobname);
86
+ };
87
+ const setting = async (name) => {
88
+ try {
89
+ const { rows } = await db.query(SETTING_SQL, [name]);
90
+ return rows[0]?.setting ?? null;
91
+ } catch {
92
+ return null;
93
+ }
94
+ };
95
+ const runIdOf = (id) => {
96
+ if (!id.startsWith(idPrefix)) return null;
97
+ const n = Number(id.slice(idPrefix.length));
98
+ return Number.isSafeInteger(n) ? n : null;
99
+ };
100
+ const retire = (host, name, definition, why) => {
101
+ const base = unscheduled(definition);
102
+ const next = { ...base, description: `${base.description ?? "pg_cron job"} (${why})` };
103
+ try {
104
+ host.job(name, next);
105
+ declared.set(name, keyOf(next));
106
+ retired.add(name);
107
+ } catch (e) {
108
+ host.onError(e, `source pg_cron: job ${name}`);
109
+ }
110
+ };
111
+ return {
112
+ name: "pg_cron",
113
+ async sync(host) {
114
+ const now = host.now();
115
+ let timezone = options.timezone;
116
+ if (!timezone) {
117
+ const tz = await setting("cron.timezone");
118
+ if (tz === null) warnOnce(host, "tz", "could not read cron.timezone; assuming UTC. Grant pg_read_all_settings or pass pgCron(pool, { timezone }).");
119
+ timezone = tz === null || /^(gmt|utc|z)$/i.test(tz) ? "UTC" : tz;
120
+ }
121
+ const logRun = await setting("cron.log_run");
122
+ const recording = logRun !== "off";
123
+ if (!recording) warnOnce(host, "log_run", "cron.log_run is off, so pg_cron records no runs: jobs are watched without their schedules and no run can fail. Turn it on to watch them.");
124
+ const { rows } = await db.query(JOBS_SQL);
125
+ if (rows.length === 0) {
126
+ warnOnce(host, "empty", "cron.job shows no jobs. pg_cron's row level security shows a role only the jobs it scheduled: connect as that role, or give this one BYPASSRLS.");
127
+ }
128
+ const all = rows.map((r) => ({ ...r, jobid: Number(r.jobid) }));
129
+ const jobs = all.filter(picks);
130
+ const names = /* @__PURE__ */ new Map();
131
+ const definitions = /* @__PURE__ */ new Map();
132
+ const used = /* @__PURE__ */ new Set();
133
+ for (const job of jobs) {
134
+ let name = prefix + (options.jobName ? options.jobName(job) : pgCronJobName(job));
135
+ if (used.has(name)) name = `${name}:${job.jobid}`;
136
+ used.add(name);
137
+ const extra = typeof options.options === "function" ? options.options(job) : options.options ?? {};
138
+ const schedule = job.active && recording ? pgCronSchedule(job.schedule) : null;
139
+ let definition = {
140
+ description: `pg_cron job ${job.jobid} in ${job.database} as ${job.username}${job.active ? "" : " (paused)"}`,
141
+ tags: ["pg_cron"],
142
+ ...extra,
143
+ ...schedule ? { schedule, timezone } : {}
144
+ };
145
+ try {
146
+ if (declared.get(name) !== keyOf(definition)) {
147
+ const key = keyOf(definition);
148
+ try {
149
+ host.job(name, definition);
150
+ } catch (e) {
151
+ if (!schedule) throw e;
152
+ host.onError(new Error(`pg_cron job ${job.jobid}: ${e.message}; watching it without a schedule`), "source pg_cron");
153
+ const { schedule: _s, timezone: _t, ...rest } = definition;
154
+ definition = rest;
155
+ host.job(name, rest);
156
+ }
157
+ declared.set(name, key);
158
+ }
159
+ names.set(job.jobid, name);
160
+ definitions.set(job.jobid, definition);
161
+ } catch (e) {
162
+ host.onError(e, `source pg_cron: job ${job.jobid}`);
163
+ }
164
+ }
165
+ const inUse = new Set(names.values());
166
+ for (const name of inUse) retired.delete(name);
167
+ for (const [jobid, previous] of known) {
168
+ if (inUse.has(previous.name)) continue;
169
+ const renamed = names.get(jobid);
170
+ retire(host, previous.name, previous.definition, renamed ? `renamed to ${renamed}` : "no longer watched");
171
+ }
172
+ known = new Map([...names].map(([jobid, name]) => [jobid, { name, definition: definitions.get(jobid) }]));
173
+ if (!scanned && rows.length > 0) {
174
+ scanned = true;
175
+ try {
176
+ const visible = new Set(all.map((j) => j.jobid));
177
+ for (const stored of await host.store.listJobs()) {
178
+ const def = stored.definition;
179
+ if (!stored.name.startsWith(prefix) || inUse.has(stored.name) || !def.schedule || !(def.tags ?? []).includes("pg_cron")) continue;
180
+ const match = /^pg_cron job (\d+) in /.exec(def.description ?? "");
181
+ if (!match) continue;
182
+ const jobid = Number(match[1]);
183
+ const current = names.get(jobid);
184
+ if (!visible.has(jobid)) retire(host, stored.name, def, "no longer in cron.job");
185
+ else if (current && !stored.name.endsWith(current.slice(prefix.length))) retire(host, stored.name, def, `renamed to ${current}`);
186
+ }
187
+ } catch (e) {
188
+ host.onError(e, "source pg_cron");
189
+ }
190
+ }
191
+ if (!recording || names.size === 0) return [];
192
+ const alerts = [];
193
+ const record = async (row, evaluate) => {
194
+ const runid = Number(row.runid);
195
+ const jobid = Number(row.jobid);
196
+ const name = pending.get(runid) ?? names.get(jobid);
197
+ if (!name) {
198
+ held.delete(runid);
199
+ return;
200
+ }
201
+ let run;
202
+ if (row.start_time === null && !finishedStatus(row.status)) {
203
+ const since = held.get(runid) ?? now;
204
+ if (now - since < PG_CRON_HOLD_MS) {
205
+ held.set(runid, since);
206
+ return;
207
+ }
208
+ run = pgCronRun({ ...row, start_time: new Date(since) }, name, idPrefix);
209
+ } else {
210
+ run = pgCronRun(row, name, idPrefix, lastAt.get(jobid) ?? now);
211
+ }
212
+ held.delete(runid);
213
+ if (!run) return;
214
+ try {
215
+ alerts.push(...await host.recordRun(run, { evaluate }));
216
+ } catch (e) {
217
+ host.onError(e, `source pg_cron: run ${runid}`);
218
+ return;
219
+ }
220
+ if (run.status === "running") pending.set(runid, name);
221
+ else pending.delete(runid);
222
+ if (run.startedAt > (lastAt.get(jobid) ?? -Infinity)) lastAt.set(jobid, run.startedAt);
223
+ };
224
+ for (const [jobid, name] of names) {
225
+ if (cursors.has(jobid)) continue;
226
+ const ours = (await host.store.listRuns(name, BACKFILL)).filter((r) => runIdOf(r.id) !== null);
227
+ if (ours.length > 0) {
228
+ cursors.set(jobid, Math.max(...ours.map((r) => runIdOf(r.id))));
229
+ lastAt.set(jobid, Math.max(...ours.map((r) => r.startedAt)));
230
+ for (const r of ours) if (r.status === "running" || r.status === "timeout") pending.set(runIdOf(r.id), r.job);
231
+ continue;
232
+ }
233
+ const { rows: newest } = await db.query(NEWEST_SQL, [jobid]);
234
+ const ordered = newest.slice().reverse();
235
+ let lastFinished = -1;
236
+ ordered.forEach((r, i) => {
237
+ if (finishedStatus(r.status)) lastFinished = i;
238
+ });
239
+ for (const [i, row] of ordered.entries()) {
240
+ if (await host.store.getRun(`${idPrefix}${row.runid}`)) continue;
241
+ await record(row, i >= lastFinished);
242
+ }
243
+ cursors.set(jobid, ordered.length > 0 ? Number(ordered[ordered.length - 1].runid) : 0);
244
+ }
245
+ const watched = /* @__PURE__ */ new Set([...names.values(), ...retired]);
246
+ for (const run of await host.store.runningRuns()) {
247
+ const id = runIdOf(run.id);
248
+ if (id !== null && watched.has(run.job)) pending.set(id, run.job);
249
+ }
250
+ const open = /* @__PURE__ */ new Set([...pending.keys(), ...held.keys()]);
251
+ let complete = false;
252
+ for (let page = 0; page < MAX_PAGES; page++) {
253
+ const jobids = [...names.keys()];
254
+ const { rows: details } = await db.query(RUNS_SQL, [jobids, jobids.map((j) => cursors.get(j) ?? 0), [...open]]);
255
+ for (const row of details) {
256
+ const jobid = Number(row.jobid);
257
+ const runid = Number(row.runid);
258
+ open.delete(runid);
259
+ await record(row, true);
260
+ if (names.has(jobid) && runid > (cursors.get(jobid) ?? 0)) cursors.set(jobid, runid);
261
+ }
262
+ if (details.length < PAGE) {
263
+ complete = true;
264
+ break;
265
+ }
266
+ }
267
+ if (complete) {
268
+ for (const runid of open) {
269
+ pending.delete(runid);
270
+ held.delete(runid);
271
+ }
272
+ }
273
+ return alerts;
274
+ }
275
+ };
276
+ }
277
+
278
+ export { PG_CRON_HOLD_MS, pgCron, pgCronJobName, pgCronRun, pgCronSchedule };
279
+ //# sourceMappingURL=pg-cron.js.map
280
+ //# sourceMappingURL=pg-cron.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sources/pgcron.ts"],"names":[],"mappings":"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","file":"pg-cron.js","sourcesContent":["import type { Source, SourceHost } from \"../client.js\";\nimport type { Alert, JobOptions, Run, StoredJobDefinition } from \"../types.js\";\n\n/**\n * Anything with a pg-style query(): a `pg` Pool or Client, or a wrapper\n * around another driver that answers with `{ rows }`.\n */\nexport interface Queryable {\n query(text: string, values?: unknown[]): Promise<{ rows: any[] }>;\n}\n\n/** A row of cron.job. */\nexport interface PgCronJob {\n jobid: number;\n jobname: string | null;\n schedule: string;\n database: string;\n username: string;\n active: boolean;\n}\n\nexport interface PgCronOptions {\n /**\n * Which jobs to watch: names or ids, or a function that picks them. Default\n * every job the role can see.\n */\n jobs?: (string | number)[] | ((job: PgCronJob) => boolean);\n /** Put before every job name, to keep them apart from your own (\"db:\"). Also keeps run ids apart. */\n prefix?: string;\n /**\n * The CronWatch name for a job. Default its jobname with anything other\n * than letters, digits, \".\", \"_\", \":\" and \"-\" turned into \"-\", or\n * \"pg_cron:<jobid>\" when it has none. The prefix goes in front either way.\n */\n jobName?: (job: PgCronJob) => string;\n /** Grace, timeout, maxDuration, expect and the rest, for every job or per job. The schedule always comes from pg_cron. */\n options?: Omit<JobOptions, \"schedule\" | \"timezone\"> | ((job: PgCronJob) => Omit<JobOptions, \"schedule\" | \"timezone\">);\n /**\n * The timezone pg_cron reads its cron expressions in. Default the server's\n * cron.timezone, read from pg_settings, which shows it only to roles with\n * pg_read_all_settings; UTC (pg_cron's default) is assumed when it cannot\n * be read.\n */\n timezone?: string;\n}\n\n/** How many of a job's newest runs are copied, without alerting, the first time it is seen. */\nconst BACKFILL = 20;\n/** Run details read per query, and the most read in one sync. */\nconst PAGE = 500;\nconst MAX_PAGES = 10;\n/**\n * How long a run pg_cron has queued but not started (no start_time yet) is\n * waited for. After that it is copied as running from when it was first\n * seen, so a run that never starts is marked stuck like any other.\n */\nexport const PG_CRON_HOLD_MS = 10 * 60_000;\n\nconst JOBS_SQL = `SELECT jobid, jobname, schedule, database, username, active FROM cron.job ORDER BY jobid`;\n// pg_settings has no row for a setting the role may not read, where\n// current_setting() raises an error that would abort the caller's transaction.\nconst SETTING_SQL = `SELECT setting FROM pg_settings WHERE name = $1`;\nconst COLUMNS = `d.runid, d.jobid, d.status, d.return_message, d.start_time, d.end_time`;\n// Every tracked job's runs after its cursor, and any run still open here, whatever its job.\nconst RUNS_SQL = `SELECT ${COLUMNS}\n FROM cron.job_run_details d\n LEFT JOIN unnest($1::bigint[], $2::bigint[]) AS c(jobid, after) ON d.jobid = c.jobid\n WHERE d.runid > c.after OR d.runid = ANY($3::bigint[])\n ORDER BY d.runid LIMIT ${PAGE}`;\nconst NEWEST_SQL = `SELECT ${COLUMNS} FROM cron.job_run_details d WHERE d.jobid = $1 ORDER BY d.runid DESC LIMIT ${BACKFILL}`;\n\ninterface DetailRow {\n runid: number | string;\n jobid: number | string;\n status: string | null;\n return_message: string | null;\n start_time: Date | string | null;\n end_time: Date | string | null;\n}\n\nconst finishedStatus = (status: string | null) => status === \"succeeded\" || status === \"failed\";\n\n/**\n * pg_cron takes a cron expression, with \"$\" for the last day of the month,\n * or \"N seconds\" for 1 to 59 seconds. Returns the CronWatch schedule, or\n * null for one that has no cadence to watch. pg_cron reads only the first\n * five fields of an expression and ignores the rest, so only those are kept\n * (a sixth would otherwise be read as seconds).\n */\nexport function pgCronSchedule(schedule: string): string | null {\n const text = schedule.trim();\n const seconds = /^(\\d+)\\s*seconds?$/i.exec(text);\n if (seconds) return `every ${Number(seconds[1])}s`;\n if (/^@reboot$/i.test(text)) return null;\n const fields = text.split(/\\s+/);\n if (fields.length > 5 && !fields[0]!.startsWith(\"@\")) fields.length = 5;\n if (fields.length === 5 && fields[2]!.includes(\"$\")) fields[2] = fields[2]!.replace(/\\$/g, \"L\");\n return fields.join(\" \");\n}\n\n/** The default CronWatch name for a pg_cron job, before the prefix. */\nexport function pgCronJobName(job: Pick<PgCronJob, \"jobid\" | \"jobname\">): string {\n const cleaned = (job.jobname ?? \"\").replace(/[^A-Za-z0-9._:-]+/g, \"-\").replace(/^[^A-Za-z0-9]+/, \"\").slice(0, 100);\n return cleaned || `pg_cron:${job.jobid}`;\n}\n\n/**\n * A row of cron.job_run_details as a CronWatch run, or null for one that has\n * not started (no start_time, not finished). A finished row with no\n * start_time (pg_cron writes these for runs a server restart cut off,\n * \"server restarted\") starts at its end_time, else at `fallbackAt` (the\n * reader passes the job's newest run's start, or now).\n */\nexport function pgCronRun(row: DetailRow, job: string, idPrefix: string, fallbackAt: number = Date.now()): Run | null {\n const finishedAt = row.end_time === null ? null : new Date(row.end_time).getTime();\n const done = finishedStatus(row.status);\n if (row.start_time === null && !done) return null;\n const startedAt = row.start_time !== null ? new Date(row.start_time).getTime() : (finishedAt ?? fallbackAt);\n const message = row.return_message === null ? null : row.return_message.trim() || null;\n const status: Run[\"status\"] = row.status === \"succeeded\" ? \"ok\" : row.status === \"failed\" ? \"failed\" : \"running\";\n const end = done ? Math.max(startedAt, finishedAt ?? startedAt) : null;\n return {\n id: `${idPrefix}${row.runid}`,\n job,\n status,\n startedAt,\n finishedAt: end,\n durationMs: end === null ? null : end - startedAt,\n error: status === \"failed\" ? (message ?? \"pg_cron reported the run as failed\") : null,\n output: status === \"ok\" ? message : null,\n metrics: {},\n trigger: \"pg_cron\",\n };\n}\n\n/** The options of a stored definition that can be declared again, without its schedule. */\nfunction unscheduled(definition: StoredJobDefinition | JobOptions): JobOptions {\n const { description, tags, grace, timeout, maxDuration, budget, failuresBeforeAlert } = definition;\n const out: JobOptions = {};\n if (description !== undefined) out.description = description;\n if (tags !== undefined) out.tags = tags;\n if (grace !== undefined) out.grace = grace;\n if (timeout !== undefined) out.timeout = timeout;\n if (maxDuration !== undefined) out.maxDuration = maxDuration;\n if (budget !== undefined) out.budget = budget;\n if (failuresBeforeAlert !== undefined) out.failuresBeforeAlert = failuresBeforeAlert;\n return out;\n}\n\nconst keyOf = (definition: JobOptions) =>\n JSON.stringify(definition, (_k, v: unknown) => (v instanceof RegExp || typeof v === \"function\" ? String(v) : v));\n\n/**\n * Watches pg_cron jobs, which run inside Postgres where nothing can wrap\n * them. As a source, on every check it reads cron.job and declares each job\n * with its schedule, then copies new rows of cron.job_run_details in as runs\n * (ids \"pgcron:<runid>\"), so the usual evaluation raises missed, failed,\n * stuck and slow alerts.\n *\n * A job that is renamed, unscheduled or no longer picked keeps its old name's\n * runs and history, and that name is declared again without a schedule, so\n * it is never reported missed. Its description says why.\n *\n * cronwatch({ store: postgres({ pool }), sources: [pgCron(pool)] }).start();\n */\nexport function pgCron(db: Queryable, options: PgCronOptions = {}): Source {\n const prefix = options.prefix ?? \"\";\n const idPrefix = `pgcron:${prefix}`;\n /** The newest runid read for each jobid, once known. */\n const cursors = new Map<number, number>();\n /** The start of the newest run copied for each jobid: where a restart row with no times is put. */\n const lastAt = new Map<number, number>();\n /** Runs copied while still going, by runid, with their job: read again until they finish, even once a check marks them timeout. */\n const pending = new Map<number, string>();\n /** Runs read before they started, by runid, with when they were first seen. */\n const held = new Map<number, number>();\n /** Each job's name and definition as last declared, by jobid. */\n let known = new Map<number, { name: string; definition: JobOptions }>();\n /** The last definition declared for each name, so an unchanged job is not declared again. */\n const declared = new Map<string, string>();\n /** Names declared again without a schedule by retire(), whose open runs are still read. */\n const retired = new Set<string>();\n let scanned = false;\n const warned = new Set<string>();\n\n const warnOnce = (host: SourceHost, key: string, message: string) => {\n if (warned.has(key)) return;\n warned.add(key);\n host.onError(new Error(message), \"source pg_cron\");\n };\n\n const picks = (job: PgCronJob): boolean => {\n const { jobs } = options;\n if (jobs === undefined) return true;\n if (typeof jobs === \"function\") return jobs(job);\n return jobs.some((j) => (typeof j === \"number\" ? j === job.jobid : j === job.jobname));\n };\n\n const setting = async (name: string): Promise<string | null> => {\n try {\n const { rows } = await db.query(SETTING_SQL, [name]);\n return (rows[0]?.setting as string | null | undefined) ?? null;\n } catch {\n return null;\n }\n };\n\n const runIdOf = (id: string): number | null => {\n if (!id.startsWith(idPrefix)) return null;\n const n = Number(id.slice(idPrefix.length));\n return Number.isSafeInteger(n) ? n : null;\n };\n\n /** Declares a name this source no longer uses for any job again, without its schedule. */\n const retire = (host: SourceHost, name: string, definition: StoredJobDefinition | JobOptions, why: string) => {\n const base = unscheduled(definition);\n const next: JobOptions = { ...base, description: `${base.description ?? \"pg_cron job\"} (${why})` };\n try {\n host.job(name, next);\n declared.set(name, keyOf(next));\n retired.add(name);\n } catch (e) {\n host.onError(e, `source pg_cron: job ${name}`);\n }\n };\n\n return {\n name: \"pg_cron\",\n async sync(host) {\n const now = host.now();\n let timezone = options.timezone;\n if (!timezone) {\n const tz = await setting(\"cron.timezone\");\n if (tz === null) warnOnce(host, \"tz\", \"could not read cron.timezone; assuming UTC. Grant pg_read_all_settings or pass pgCron(pool, { timezone }).\");\n timezone = tz === null || /^(gmt|utc|z)$/i.test(tz) ? \"UTC\" : tz;\n }\n const logRun = await setting(\"cron.log_run\");\n const recording = logRun !== \"off\";\n if (!recording) warnOnce(host, \"log_run\", \"cron.log_run is off, so pg_cron records no runs: jobs are watched without their schedules and no run can fail. Turn it on to watch them.\");\n\n const { rows } = await db.query(JOBS_SQL);\n if (rows.length === 0) {\n warnOnce(host, \"empty\", \"cron.job shows no jobs. pg_cron's row level security shows a role only the jobs it scheduled: connect as that role, or give this one BYPASSRLS.\");\n }\n const all = (rows as PgCronJob[]).map((r) => ({ ...r, jobid: Number(r.jobid) }));\n const jobs = all.filter(picks);\n\n // Declare each job. A paused one (active = false) keeps its failures but loses its schedule, so it is not missed.\n const names = new Map<number, string>();\n const definitions = new Map<number, JobOptions>();\n const used = new Set<string>();\n for (const job of jobs) {\n let name = prefix + (options.jobName ? options.jobName(job) : pgCronJobName(job));\n if (used.has(name)) name = `${name}:${job.jobid}`;\n used.add(name);\n const extra = typeof options.options === \"function\" ? options.options(job) : (options.options ?? {});\n const schedule = job.active && recording ? pgCronSchedule(job.schedule) : null;\n let definition: JobOptions = {\n description: `pg_cron job ${job.jobid} in ${job.database} as ${job.username}${job.active ? \"\" : \" (paused)\"}`,\n tags: [\"pg_cron\"],\n ...extra,\n ...(schedule ? { schedule, timezone } : {}),\n };\n try {\n if (declared.get(name) !== keyOf(definition)) {\n const key = keyOf(definition);\n try {\n host.job(name, definition);\n } catch (e) {\n if (!schedule) throw e;\n // A schedule CronWatch cannot read: watch the runs, not the cadence.\n host.onError(new Error(`pg_cron job ${job.jobid}: ${(e as Error).message}; watching it without a schedule`), \"source pg_cron\");\n const { schedule: _s, timezone: _t, ...rest } = definition;\n definition = rest;\n host.job(name, rest);\n }\n declared.set(name, key);\n }\n names.set(job.jobid, name);\n definitions.set(job.jobid, definition);\n } catch (e) {\n host.onError(e, `source pg_cron: job ${job.jobid}`);\n }\n }\n\n // A name this source used for a job that has since been renamed, unscheduled or dropped from `jobs`.\n const inUse = new Set(names.values());\n for (const name of inUse) retired.delete(name);\n for (const [jobid, previous] of known) {\n if (inUse.has(previous.name)) continue;\n const renamed = names.get(jobid);\n retire(host, previous.name, previous.definition, renamed ? `renamed to ${renamed}` : \"no longer watched\");\n }\n known = new Map([...names].map(([jobid, name]) => [jobid, { name, definition: definitions.get(jobid)! }]));\n // Once per process, the same for names left scheduled in the store while no process was watching.\n if (!scanned && rows.length > 0) {\n scanned = true;\n try {\n const visible = new Set(all.map((j) => j.jobid));\n for (const stored of await host.store.listJobs()) {\n const def = stored.definition;\n if (!stored.name.startsWith(prefix) || inUse.has(stored.name) || !def.schedule || !(def.tags ?? []).includes(\"pg_cron\")) continue;\n const match = /^pg_cron job (\\d+) in /.exec(def.description ?? \"\");\n if (!match) continue;\n const jobid = Number(match[1]);\n const current = names.get(jobid);\n if (!visible.has(jobid)) retire(host, stored.name, def, \"no longer in cron.job\");\n // Another pg_cron source's name for the same job ends the same way: that one is left alone.\n else if (current && !stored.name.endsWith(current.slice(prefix.length))) retire(host, stored.name, def, `renamed to ${current}`);\n }\n } catch (e) {\n host.onError(e, \"source pg_cron\");\n }\n }\n if (!recording || names.size === 0) return [];\n\n const alerts: Alert[] = [];\n /** Copies one row. A row that cannot be recorded is reported and skipped; it never stops the others. */\n const record = async (row: DetailRow, evaluate: boolean): Promise<void> => {\n const runid = Number(row.runid);\n const jobid = Number(row.jobid);\n const name = pending.get(runid) ?? names.get(jobid);\n if (!name) {\n held.delete(runid);\n return;\n }\n let run: Run | null;\n if (row.start_time === null && !finishedStatus(row.status)) {\n const since = held.get(runid) ?? now;\n if (now - since < PG_CRON_HOLD_MS) {\n held.set(runid, since);\n return;\n }\n run = pgCronRun({ ...row, start_time: new Date(since) }, name, idPrefix);\n } else {\n run = pgCronRun(row, name, idPrefix, lastAt.get(jobid) ?? now);\n }\n held.delete(runid);\n if (!run) return;\n try {\n alerts.push(...(await host.recordRun(run, { evaluate })));\n } catch (e) {\n host.onError(e, `source pg_cron: run ${runid}`);\n return;\n }\n if (run.status === \"running\") pending.set(runid, name);\n else pending.delete(runid);\n if (run.startedAt > (lastAt.get(jobid) ?? -Infinity)) lastAt.set(jobid, run.startedAt);\n };\n\n // Where each job left off. Found from the store the first time, so a restart carries on.\n for (const [jobid, name] of names) {\n if (cursors.has(jobid)) continue;\n const ours = (await host.store.listRuns(name, BACKFILL)).filter((r) => runIdOf(r.id) !== null);\n if (ours.length > 0) {\n cursors.set(jobid, Math.max(...ours.map((r) => runIdOf(r.id)!)));\n lastAt.set(jobid, Math.max(...ours.map((r) => r.startedAt)));\n for (const r of ours) if (r.status === \"running\" || r.status === \"timeout\") pending.set(runIdOf(r.id)!, r.job);\n continue;\n }\n // First sight: copy recent history quietly, and judge only from the newest finished run on.\n // The cursor goes to the newest row read, whatever is held, so history is never judged later.\n const { rows: newest } = await db.query(NEWEST_SQL, [jobid]);\n const ordered = (newest as DetailRow[]).slice().reverse();\n let lastFinished = -1;\n ordered.forEach((r, i) => { if (finishedStatus(r.status)) lastFinished = i; });\n for (const [i, row] of ordered.entries()) {\n // Already copied under another name (the job was renamed while no process watched): left there.\n if (await host.store.getRun(`${idPrefix}${row.runid}`)) continue;\n await record(row, i >= lastFinished);\n }\n cursors.set(jobid, ordered.length > 0 ? Number(ordered[ordered.length - 1]!.runid) : 0);\n }\n\n // New runs, runs copied while still going (or since marked timeout), and runs not yet started.\n const watched = new Set([...names.values(), ...retired]);\n for (const run of await host.store.runningRuns()) {\n const id = runIdOf(run.id);\n if (id !== null && watched.has(run.job)) pending.set(id, run.job);\n }\n const open = new Set([...pending.keys(), ...held.keys()]);\n let complete = false;\n for (let page = 0; page < MAX_PAGES; page++) {\n const jobids = [...names.keys()];\n const { rows: details } = await db.query(RUNS_SQL, [jobids, jobids.map((j) => cursors.get(j) ?? 0), [...open]]);\n for (const row of details as DetailRow[]) {\n const jobid = Number(row.jobid);\n const runid = Number(row.runid);\n open.delete(runid);\n await record(row, true);\n // Held or not, the cursor moves on: a held run is read again by its runid.\n if (names.has(jobid) && runid > (cursors.get(jobid) ?? 0)) cursors.set(jobid, runid);\n }\n if (details.length < PAGE) {\n complete = true;\n break;\n }\n }\n // Every row was read and these were not among them: pg_cron no longer has them.\n if (complete) {\n for (const runid of open) {\n pending.delete(runid);\n held.delete(runid);\n }\n }\n return alerts;\n },\n };\n}\n"]}
package/dist/postgres.cjs CHANGED
@@ -87,6 +87,11 @@ function statements(dialect, p) {
87
87
  }
88
88
  return sql;
89
89
  }
90
+ function updateRunIfSql(dialect, p, count) {
91
+ const text = `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ? AND status IN (${Array.from({ length: count }, () => "?").join(", ")})`;
92
+ let n = 0;
93
+ return text.replace(/\?/g, () => `$${++n}`);
94
+ }
90
95
  var params = {
91
96
  upsertJob: (definition, now) => [definition.name, JSON.stringify(definition), now, now],
92
97
  insertRun: (run) => [
@@ -102,6 +107,16 @@ var params = {
102
107
  run.trigger
103
108
  ],
104
109
  updateRun: (run) => [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],
110
+ updateRunIf: (run, fromStatuses) => [
111
+ run.status,
112
+ run.finishedAt,
113
+ run.durationMs,
114
+ run.error,
115
+ run.output,
116
+ JSON.stringify(run.metrics),
117
+ run.id,
118
+ ...fromStatuses
119
+ ],
105
120
  setState: (state) => [state.job, JSON.stringify(state)],
106
121
  casInsert: (state) => [state.job, JSON.stringify(state)],
107
122
  casUpdate: (state, expectedVersion) => [JSON.stringify(state), state.job, expectedVersion]
@@ -179,6 +194,11 @@ function postgres(options = {}) {
179
194
  async updateRun(run) {
180
195
  await pool.query(sql.updateRun, params.updateRun(run));
181
196
  },
197
+ async updateRunIf(run, fromStatuses) {
198
+ if (fromStatuses.length === 0) return false;
199
+ const result = await pool.query(updateRunIfSql("postgres", p, fromStatuses.length), params.updateRunIf(run, fromStatuses));
200
+ return (result.rowCount ?? 0) > 0;
201
+ },
182
202
  async getRun(id) {
183
203
  const { rows } = await pool.query(sql.getRun, [id]);
184
204
  return rows[0] ? rowToRun(rows[0]) : null;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/stores/sql.ts","../src/stores/postgres.ts"],"names":["json","pg"],"mappings":"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","file":"postgres.cjs","sourcesContent":["import type { JobState, Run, StoredJob, StoredJobDefinition } from \"../types.js\";\n\n/**\n * The schema, statements and row mapping shared by the SQLite and Postgres\n * stores. Imports no driver, so either entry point can pull it in alone.\n * Statements are written with `?` placeholders; Postgres numbers them.\n */\n\nexport type Dialect = \"sqlite\" | \"postgres\";\n\nexport const DEFAULT_PREFIX = \"cronwatch_\";\n\n// Postgres truncates identifiers past 63 bytes; the longest name we build is the prefix plus \"runs_job_started\".\nconst MAX_PREFIX = 63 - \"runs_job_started\".length;\n\n/**\n * Table names are built from the prefix, so it must be a plain lowercase\n * identifier. Uppercase is refused rather than folded: Postgres lowercases\n * unquoted names, so \"Monitoring_\" would quietly become \"monitoring_\".\n */\nexport function tablePrefix(prefix: string = DEFAULT_PREFIX): string {\n if (!/^[a-z_][a-z0-9_]*$/.test(prefix) || prefix.length > MAX_PREFIX) {\n throw new Error(\n `cronwatch: invalid table prefix ${JSON.stringify(prefix)}. Use lowercase letters, digits and underscores, ` +\n `not starting with a digit, at most ${MAX_PREFIX} characters.`,\n );\n }\n return prefix;\n}\n\nexport function schema(dialect: Dialect, p: string): string {\n const pg = dialect === \"postgres\";\n const int = pg ? \"BIGINT\" : \"INTEGER\";\n const json = pg ? \"JSONB\" : \"TEXT\";\n return `\n CREATE TABLE IF NOT EXISTS ${p}jobs (\n name TEXT PRIMARY KEY,\n definition ${json} NOT NULL,\n created_at ${int} NOT NULL,\n updated_at ${int} NOT NULL\n );\n CREATE TABLE IF NOT EXISTS ${p}runs (${pg ? \"\\n seq BIGSERIAL,\" : \"\"}\n id TEXT PRIMARY KEY,\n job TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at ${int} NOT NULL,\n finished_at ${int},\n duration_ms ${int},\n error TEXT,\n output TEXT,\n metrics ${json} NOT NULL DEFAULT '{}',\n trigger TEXT NOT NULL DEFAULT 'run'\n );\n CREATE INDEX IF NOT EXISTS ${p}runs_job_started ON ${p}runs (job, started_at DESC);\n CREATE INDEX IF NOT EXISTS ${p}runs_running ON ${p}runs (status) WHERE status = 'running';\n CREATE TABLE IF NOT EXISTS ${p}state (\n job TEXT PRIMARY KEY,\n state ${json} NOT NULL\n );\n `;\n}\n\nexport function statements(dialect: Dialect, p: string) {\n const pg = dialect === \"postgres\";\n // Insertion order, to break ties between runs that started in the same millisecond.\n const seq = pg ? \"seq\" : \"rowid\";\n // Byte order on both, so names sort the same whatever the database's collation.\n const byName = pg ? `name COLLATE \"C\"` : \"name\";\n // The version inside a state's JSON, 0 when it has none.\n const version = (column: string) => (pg ? `COALESCE((${column}->>'version')::bigint, 0)` : `COALESCE(json_extract(${column}, '$.version'), 0)`);\n const sql = {\n upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,\n getJob: `SELECT * FROM ${p}jobs WHERE name = ?`,\n listJobs: `SELECT * FROM ${p}jobs ORDER BY ${byName}`,\n deleteRuns: `DELETE FROM ${p}runs WHERE job = ?`,\n deleteState: `DELETE FROM ${p}state WHERE job = ?`,\n deleteJob: `DELETE FROM ${p}jobs WHERE name = ?`,\n insertRun: `INSERT INTO ${p}runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n updateRun: `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?`,\n getRun: `SELECT * FROM ${p}runs WHERE id = ?`,\n listRuns: `SELECT * FROM ${p}runs WHERE job = ? ORDER BY started_at DESC, ${seq} DESC LIMIT ?`,\n runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,\n getState: `SELECT state FROM ${p}state WHERE job = ?`,\n setState: `INSERT INTO ${p}state (job, state) VALUES (?, ?) ON CONFLICT (job) DO UPDATE SET state = excluded.state`,\n // compareAndSetState. Expecting version 0 also matches a missing row, so\n // that case inserts; any other version must find its row.\n casInsert: `INSERT INTO ${p}state (job, state) VALUES (?, ?)\n ON CONFLICT (job) DO UPDATE SET state = excluded.state WHERE ${version(`${p}state.state`)} = 0`,\n casUpdate: `UPDATE ${p}state SET state = ? WHERE job = ? AND ${version(\"state\")} = ?`,\n // Each job's newest run is kept whatever its age: without it, a job that\n // runs less often than the retention looks like it never ran.\n prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?\n AND started_at < (SELECT MAX(r.started_at) FROM ${p}runs r WHERE r.job = ${p}runs.job)`,\n };\n if (pg) {\n for (const key of Object.keys(sql) as (keyof typeof sql)[]) {\n let n = 0;\n sql[key] = sql[key].replace(/\\?/g, () => `$${++n}`);\n }\n }\n return sql;\n}\n\n// Parameters in statement order, so both drivers bind the same values.\nexport const params = {\n upsertJob: (definition: StoredJobDefinition, now: number) => [definition.name, JSON.stringify(definition), now, now],\n insertRun: (run: Run) => [\n run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.trigger,\n ],\n updateRun: (run: Run) => [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],\n setState: (state: JobState) => [state.job, JSON.stringify(state)],\n casInsert: (state: JobState) => [state.job, JSON.stringify(state)],\n casUpdate: (state: JobState, expectedVersion: number) => [JSON.stringify(state), state.job, expectedVersion],\n};\n\n// SQLite hands back JSON as TEXT and Postgres as parsed JSONB; Postgres returns BIGINT as a string.\ntype Json<T> = T | string;\ntype Int = number | string;\n\nexport interface JobRow { name: string; definition: Json<StoredJobDefinition>; created_at: Int; updated_at: Int }\nexport interface RunRow {\n id: string; job: string; status: Run[\"status\"]; started_at: Int; finished_at: Int | null;\n duration_ms: Int | null; error: string | null; output: string | null; metrics: Json<Record<string, number>> | null; trigger: string;\n}\nexport interface StateRow { state: Json<JobState> }\n\nconst json = <T>(v: Json<T>): T => (typeof v === \"string\" ? (JSON.parse(v) as T) : v);\nconst num = (v: Int | null): number | null => (v === null ? null : Number(v));\n\nexport function rowToJob(r: JobRow): StoredJob {\n return { name: r.name, definition: json(r.definition), createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) };\n}\n\nexport function rowToRun(r: RunRow): Run {\n return {\n id: r.id, job: r.job, status: r.status, startedAt: Number(r.started_at), finishedAt: num(r.finished_at),\n durationMs: num(r.duration_ms), error: r.error, output: r.output, metrics: r.metrics === null ? {} : json(r.metrics), trigger: r.trigger,\n };\n}\n\nexport function rowToState(r: StateRow): JobState {\n return json(r.state);\n}\n","import pg from \"pg\";\nimport type { Store, StoredJob } from \"../types.js\";\nimport { params, rowToJob, rowToRun, rowToState, schema, statements, tablePrefix, type JobRow, type RunRow, type StateRow } from \"./sql.js\";\n\nexport interface PostgresOptions {\n /** postgres://... Defaults to process.env.DATABASE_URL. */\n connectionString?: string;\n /** Bring your own pool instead. It is not closed by close(). */\n pool?: pg.Pool;\n /** Table name prefix: lowercase letters, digits and underscores. Default \"cronwatch_\". */\n prefix?: string;\n}\n\n/**\n * Keeps everything in Postgres through the `pg` driver. For apps on Vercel,\n * Neon, Supabase, Railway and the like, where there is no disk to keep a\n * SQLite file on. Times are stored as BIGINT epoch milliseconds.\n */\nexport function postgres(options: PostgresOptions = {}): Store {\n const p = tablePrefix(options.prefix);\n const sql = statements(\"postgres\", p);\n const owned = !options.pool;\n const pool = options.pool ?? new pg.Pool({ connectionString: options.connectionString ?? process.env.DATABASE_URL });\n\n const transaction = async (work: (client: pg.PoolClient) => Promise<void>) => {\n const client = await pool.connect();\n try {\n await client.query(\"BEGIN\");\n await work(client);\n await client.query(\"COMMIT\");\n } catch (e) {\n await client.query(\"ROLLBACK\");\n throw e;\n } finally {\n client.release();\n }\n };\n\n return {\n async init() {\n // Many instances starting at once would race CREATE TABLE IF NOT EXISTS, which Postgres can\n // reject with a unique violation on pg_type. A lock per prefix makes them take turns.\n await transaction(async (client) => {\n await client.query(\"SELECT pg_advisory_xact_lock(hashtext($1))\", [`cronwatch:${p}`]);\n await client.query(schema(\"postgres\", p));\n });\n },\n async upsertJob(definition, now) {\n await pool.query(sql.upsertJob, params.upsertJob(definition, now));\n },\n async getJob(name) {\n const { rows } = await pool.query<JobRow>(sql.getJob, [name]);\n return rows[0] ? rowToJob(rows[0]) : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n const { rows } = await pool.query<JobRow>(sql.listJobs);\n return rows.map(rowToJob);\n },\n async deleteJob(name) {\n await transaction(async (client) => {\n await client.query(sql.deleteRuns, [name]);\n await client.query(sql.deleteState, [name]);\n await client.query(sql.deleteJob, [name]);\n });\n },\n async insertRun(run) {\n await pool.query(sql.insertRun, params.insertRun(run));\n },\n async updateRun(run) {\n await pool.query(sql.updateRun, params.updateRun(run));\n },\n async getRun(id) {\n const { rows } = await pool.query<RunRow>(sql.getRun, [id]);\n return rows[0] ? rowToRun(rows[0]) : null;\n },\n async listRuns(job, limit) {\n const { rows } = await pool.query<RunRow>(sql.listRuns, [job, limit]);\n return rows.map(rowToRun);\n },\n async lastRun(job) {\n const { rows } = await pool.query<RunRow>(sql.listRuns, [job, 1]);\n return rows[0] ? rowToRun(rows[0]) : null;\n },\n async runningRuns() {\n const { rows } = await pool.query<RunRow>(sql.runningRuns);\n return rows.map(rowToRun);\n },\n async getState(job) {\n const { rows } = await pool.query<StateRow>(sql.getState, [job]);\n return rows[0] ? rowToState(rows[0]) : null;\n },\n async setState(state) {\n await pool.query(sql.setState, params.setState(state));\n },\n async compareAndSetState(state, expectedVersion) {\n const result = expectedVersion === 0\n ? await pool.query(sql.casInsert, params.casInsert(state))\n : await pool.query(sql.casUpdate, params.casUpdate(state, expectedVersion));\n return (result.rowCount ?? 0) > 0;\n },\n async prune(before) {\n const result = await pool.query(sql.prune, [before]);\n return result.rowCount ?? 0;\n },\n async close() {\n if (owned) await pool.end();\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/stores/sql.ts","../src/stores/postgres.ts"],"names":["json","pg"],"mappings":"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","file":"postgres.cjs","sourcesContent":["import type { JobState, Run, StoredJob, StoredJobDefinition } from \"../types.js\";\n\n/**\n * The schema, statements and row mapping shared by the SQLite and Postgres\n * stores. Imports no driver, so either entry point can pull it in alone.\n * Statements are written with `?` placeholders; Postgres numbers them.\n */\n\nexport type Dialect = \"sqlite\" | \"postgres\";\n\nexport const DEFAULT_PREFIX = \"cronwatch_\";\n\n// Postgres truncates identifiers past 63 bytes; the longest name we build is the prefix plus \"runs_job_started\".\nconst MAX_PREFIX = 63 - \"runs_job_started\".length;\n\n/**\n * Table names are built from the prefix, so it must be a plain lowercase\n * identifier. Uppercase is refused rather than folded: Postgres lowercases\n * unquoted names, so \"Monitoring_\" would quietly become \"monitoring_\".\n */\nexport function tablePrefix(prefix: string = DEFAULT_PREFIX): string {\n if (!/^[a-z_][a-z0-9_]*$/.test(prefix) || prefix.length > MAX_PREFIX) {\n throw new Error(\n `cronwatch: invalid table prefix ${JSON.stringify(prefix)}. Use lowercase letters, digits and underscores, ` +\n `not starting with a digit, at most ${MAX_PREFIX} characters.`,\n );\n }\n return prefix;\n}\n\nexport function schema(dialect: Dialect, p: string): string {\n const pg = dialect === \"postgres\";\n const int = pg ? \"BIGINT\" : \"INTEGER\";\n const json = pg ? \"JSONB\" : \"TEXT\";\n return `\n CREATE TABLE IF NOT EXISTS ${p}jobs (\n name TEXT PRIMARY KEY,\n definition ${json} NOT NULL,\n created_at ${int} NOT NULL,\n updated_at ${int} NOT NULL\n );\n CREATE TABLE IF NOT EXISTS ${p}runs (${pg ? \"\\n seq BIGSERIAL,\" : \"\"}\n id TEXT PRIMARY KEY,\n job TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at ${int} NOT NULL,\n finished_at ${int},\n duration_ms ${int},\n error TEXT,\n output TEXT,\n metrics ${json} NOT NULL DEFAULT '{}',\n trigger TEXT NOT NULL DEFAULT 'run'\n );\n CREATE INDEX IF NOT EXISTS ${p}runs_job_started ON ${p}runs (job, started_at DESC);\n CREATE INDEX IF NOT EXISTS ${p}runs_running ON ${p}runs (status) WHERE status = 'running';\n CREATE TABLE IF NOT EXISTS ${p}state (\n job TEXT PRIMARY KEY,\n state ${json} NOT NULL\n );\n `;\n}\n\nexport function statements(dialect: Dialect, p: string) {\n const pg = dialect === \"postgres\";\n // Insertion order, to break ties between runs that started in the same millisecond.\n const seq = pg ? \"seq\" : \"rowid\";\n // Byte order on both, so names sort the same whatever the database's collation.\n const byName = pg ? `name COLLATE \"C\"` : \"name\";\n // The version inside a state's JSON, 0 when it has none.\n const version = (column: string) => (pg ? `COALESCE((${column}->>'version')::bigint, 0)` : `COALESCE(json_extract(${column}, '$.version'), 0)`);\n const sql = {\n upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,\n getJob: `SELECT * FROM ${p}jobs WHERE name = ?`,\n listJobs: `SELECT * FROM ${p}jobs ORDER BY ${byName}`,\n deleteRuns: `DELETE FROM ${p}runs WHERE job = ?`,\n deleteState: `DELETE FROM ${p}state WHERE job = ?`,\n deleteJob: `DELETE FROM ${p}jobs WHERE name = ?`,\n insertRun: `INSERT INTO ${p}runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n updateRun: `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?`,\n getRun: `SELECT * FROM ${p}runs WHERE id = ?`,\n listRuns: `SELECT * FROM ${p}runs WHERE job = ? ORDER BY started_at DESC, ${seq} DESC LIMIT ?`,\n runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,\n getState: `SELECT state FROM ${p}state WHERE job = ?`,\n setState: `INSERT INTO ${p}state (job, state) VALUES (?, ?) ON CONFLICT (job) DO UPDATE SET state = excluded.state`,\n // compareAndSetState. Expecting version 0 also matches a missing row, so\n // that case inserts; any other version must find its row.\n casInsert: `INSERT INTO ${p}state (job, state) VALUES (?, ?)\n ON CONFLICT (job) DO UPDATE SET state = excluded.state WHERE ${version(`${p}state.state`)} = 0`,\n casUpdate: `UPDATE ${p}state SET state = ? WHERE job = ? AND ${version(\"state\")} = ?`,\n // Each job's newest run is kept whatever its age: without it, a job that\n // runs less often than the retention looks like it never ran.\n prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?\n AND started_at < (SELECT MAX(r.started_at) FROM ${p}runs r WHERE r.job = ${p}runs.job)`,\n };\n if (pg) {\n for (const key of Object.keys(sql) as (keyof typeof sql)[]) {\n let n = 0;\n sql[key] = sql[key].replace(/\\?/g, () => `$${++n}`);\n }\n }\n return sql;\n}\n\n/**\n * updateRunIf: the update above, only while the stored status is one of\n * `count` statuses. Built per count, since the list is bound value by value.\n */\nexport function updateRunIfSql(dialect: Dialect, p: string, count: number): string {\n const text = `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ? AND status IN (${Array.from({ length: count }, () => \"?\").join(\", \")})`;\n if (dialect !== \"postgres\") return text;\n let n = 0;\n return text.replace(/\\?/g, () => `$${++n}`);\n}\n\n// Parameters in statement order, so both drivers bind the same values.\nexport const params = {\n upsertJob: (definition: StoredJobDefinition, now: number) => [definition.name, JSON.stringify(definition), now, now],\n insertRun: (run: Run) => [\n run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.trigger,\n ],\n updateRun: (run: Run) => [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],\n updateRunIf: (run: Run, fromStatuses: Run[\"status\"][]) => [\n run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id, ...fromStatuses,\n ],\n setState: (state: JobState) => [state.job, JSON.stringify(state)],\n casInsert: (state: JobState) => [state.job, JSON.stringify(state)],\n casUpdate: (state: JobState, expectedVersion: number) => [JSON.stringify(state), state.job, expectedVersion],\n};\n\n// SQLite hands back JSON as TEXT and Postgres as parsed JSONB; Postgres returns BIGINT as a string.\ntype Json<T> = T | string;\ntype Int = number | string;\n\nexport interface JobRow { name: string; definition: Json<StoredJobDefinition>; created_at: Int; updated_at: Int }\nexport interface RunRow {\n id: string; job: string; status: Run[\"status\"]; started_at: Int; finished_at: Int | null;\n duration_ms: Int | null; error: string | null; output: string | null; metrics: Json<Record<string, number>> | null; trigger: string;\n}\nexport interface StateRow { state: Json<JobState> }\n\nconst json = <T>(v: Json<T>): T => (typeof v === \"string\" ? (JSON.parse(v) as T) : v);\nconst num = (v: Int | null): number | null => (v === null ? null : Number(v));\n\nexport function rowToJob(r: JobRow): StoredJob {\n return { name: r.name, definition: json(r.definition), createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) };\n}\n\nexport function rowToRun(r: RunRow): Run {\n return {\n id: r.id, job: r.job, status: r.status, startedAt: Number(r.started_at), finishedAt: num(r.finished_at),\n durationMs: num(r.duration_ms), error: r.error, output: r.output, metrics: r.metrics === null ? {} : json(r.metrics), trigger: r.trigger,\n };\n}\n\nexport function rowToState(r: StateRow): JobState {\n return json(r.state);\n}\n","import pg from \"pg\";\nimport type { Store, StoredJob } from \"../types.js\";\nimport { params, rowToJob, rowToRun, rowToState, schema, statements, tablePrefix, updateRunIfSql, type JobRow, type RunRow, type StateRow } from \"./sql.js\";\n\nexport interface PostgresOptions {\n /** postgres://... Defaults to process.env.DATABASE_URL. */\n connectionString?: string;\n /** Bring your own pool instead. It is not closed by close(). */\n pool?: pg.Pool;\n /** Table name prefix: lowercase letters, digits and underscores. Default \"cronwatch_\". */\n prefix?: string;\n}\n\n/**\n * Keeps everything in Postgres through the `pg` driver. For apps on Vercel,\n * Neon, Supabase, Railway and the like, where there is no disk to keep a\n * SQLite file on. Times are stored as BIGINT epoch milliseconds.\n */\nexport function postgres(options: PostgresOptions = {}): Store {\n const p = tablePrefix(options.prefix);\n const sql = statements(\"postgres\", p);\n const owned = !options.pool;\n const pool = options.pool ?? new pg.Pool({ connectionString: options.connectionString ?? process.env.DATABASE_URL });\n\n const transaction = async (work: (client: pg.PoolClient) => Promise<void>) => {\n const client = await pool.connect();\n try {\n await client.query(\"BEGIN\");\n await work(client);\n await client.query(\"COMMIT\");\n } catch (e) {\n await client.query(\"ROLLBACK\");\n throw e;\n } finally {\n client.release();\n }\n };\n\n return {\n async init() {\n // Many instances starting at once would race CREATE TABLE IF NOT EXISTS, which Postgres can\n // reject with a unique violation on pg_type. A lock per prefix makes them take turns.\n await transaction(async (client) => {\n await client.query(\"SELECT pg_advisory_xact_lock(hashtext($1))\", [`cronwatch:${p}`]);\n await client.query(schema(\"postgres\", p));\n });\n },\n async upsertJob(definition, now) {\n await pool.query(sql.upsertJob, params.upsertJob(definition, now));\n },\n async getJob(name) {\n const { rows } = await pool.query<JobRow>(sql.getJob, [name]);\n return rows[0] ? rowToJob(rows[0]) : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n const { rows } = await pool.query<JobRow>(sql.listJobs);\n return rows.map(rowToJob);\n },\n async deleteJob(name) {\n await transaction(async (client) => {\n await client.query(sql.deleteRuns, [name]);\n await client.query(sql.deleteState, [name]);\n await client.query(sql.deleteJob, [name]);\n });\n },\n async insertRun(run) {\n await pool.query(sql.insertRun, params.insertRun(run));\n },\n async updateRun(run) {\n await pool.query(sql.updateRun, params.updateRun(run));\n },\n async updateRunIf(run, fromStatuses) {\n if (fromStatuses.length === 0) return false;\n const result = await pool.query(updateRunIfSql(\"postgres\", p, fromStatuses.length), params.updateRunIf(run, fromStatuses));\n return (result.rowCount ?? 0) > 0;\n },\n async getRun(id) {\n const { rows } = await pool.query<RunRow>(sql.getRun, [id]);\n return rows[0] ? rowToRun(rows[0]) : null;\n },\n async listRuns(job, limit) {\n const { rows } = await pool.query<RunRow>(sql.listRuns, [job, limit]);\n return rows.map(rowToRun);\n },\n async lastRun(job) {\n const { rows } = await pool.query<RunRow>(sql.listRuns, [job, 1]);\n return rows[0] ? rowToRun(rows[0]) : null;\n },\n async runningRuns() {\n const { rows } = await pool.query<RunRow>(sql.runningRuns);\n return rows.map(rowToRun);\n },\n async getState(job) {\n const { rows } = await pool.query<StateRow>(sql.getState, [job]);\n return rows[0] ? rowToState(rows[0]) : null;\n },\n async setState(state) {\n await pool.query(sql.setState, params.setState(state));\n },\n async compareAndSetState(state, expectedVersion) {\n const result = expectedVersion === 0\n ? await pool.query(sql.casInsert, params.casInsert(state))\n : await pool.query(sql.casUpdate, params.casUpdate(state, expectedVersion));\n return (result.rowCount ?? 0) > 0;\n },\n async prune(before) {\n const result = await pool.query(sql.prune, [before]);\n return result.rowCount ?? 0;\n },\n async close() {\n if (owned) await pool.end();\n },\n };\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import pg from 'pg';
2
- import { S as Store } from './types-DUt2apl0.cjs';
2
+ import { S as Store } from './types-Ddq1MUPL.cjs';
3
3
 
4
4
  interface PostgresOptions {
5
5
  /** postgres://... Defaults to process.env.DATABASE_URL. */
@@ -1,5 +1,5 @@
1
1
  import pg from 'pg';
2
- import { S as Store } from './types-DUt2apl0.js';
2
+ import { S as Store } from './types-Ddq1MUPL.js';
3
3
 
4
4
  interface PostgresOptions {
5
5
  /** postgres://... Defaults to process.env.DATABASE_URL. */
package/dist/postgres.js CHANGED
@@ -81,6 +81,11 @@ function statements(dialect, p) {
81
81
  }
82
82
  return sql;
83
83
  }
84
+ function updateRunIfSql(dialect, p, count) {
85
+ const text = `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ? AND status IN (${Array.from({ length: count }, () => "?").join(", ")})`;
86
+ let n = 0;
87
+ return text.replace(/\?/g, () => `$${++n}`);
88
+ }
84
89
  var params = {
85
90
  upsertJob: (definition, now) => [definition.name, JSON.stringify(definition), now, now],
86
91
  insertRun: (run) => [
@@ -96,6 +101,16 @@ var params = {
96
101
  run.trigger
97
102
  ],
98
103
  updateRun: (run) => [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],
104
+ updateRunIf: (run, fromStatuses) => [
105
+ run.status,
106
+ run.finishedAt,
107
+ run.durationMs,
108
+ run.error,
109
+ run.output,
110
+ JSON.stringify(run.metrics),
111
+ run.id,
112
+ ...fromStatuses
113
+ ],
99
114
  setState: (state) => [state.job, JSON.stringify(state)],
100
115
  casInsert: (state) => [state.job, JSON.stringify(state)],
101
116
  casUpdate: (state, expectedVersion) => [JSON.stringify(state), state.job, expectedVersion]
@@ -173,6 +188,11 @@ function postgres(options = {}) {
173
188
  async updateRun(run) {
174
189
  await pool.query(sql.updateRun, params.updateRun(run));
175
190
  },
191
+ async updateRunIf(run, fromStatuses) {
192
+ if (fromStatuses.length === 0) return false;
193
+ const result = await pool.query(updateRunIfSql("postgres", p, fromStatuses.length), params.updateRunIf(run, fromStatuses));
194
+ return (result.rowCount ?? 0) > 0;
195
+ },
176
196
  async getRun(id) {
177
197
  const { rows } = await pool.query(sql.getRun, [id]);
178
198
  return rows[0] ? rowToRun(rows[0]) : null;