@cronwatch/sdk 0.8.0 → 0.9.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 (69) hide show
  1. package/dist/anthropic.d.cts +1 -1
  2. package/dist/anthropic.d.ts +1 -1
  3. package/dist/bugsnag.d.cts +1 -1
  4. package/dist/bugsnag.d.ts +1 -1
  5. package/dist/{client-B5hM4l-D.d.ts → client-CQm-8-mR.d.ts} +13 -2
  6. package/dist/{client-Di5HAWQz.d.cts → client-D35cfnNV.d.cts} +13 -2
  7. package/dist/d1.cjs +29 -19
  8. package/dist/d1.cjs.map +1 -1
  9. package/dist/d1.d.cts +1 -1
  10. package/dist/d1.d.ts +1 -1
  11. package/dist/d1.js +29 -19
  12. package/dist/d1.js.map +1 -1
  13. package/dist/datadog.d.cts +1 -1
  14. package/dist/datadog.d.ts +1 -1
  15. package/dist/discord.d.cts +1 -1
  16. package/dist/discord.d.ts +1 -1
  17. package/dist/{email-Ciq7GRn4.d.ts → email-BxCp9RQV.d.ts} +1 -1
  18. package/dist/{email-DX6gkd5H.d.cts → email-D1d5niMy.d.cts} +1 -1
  19. package/dist/honeybadger.d.cts +1 -1
  20. package/dist/honeybadger.d.ts +1 -1
  21. package/dist/index.cjs +50 -15
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +4 -4
  24. package/dist/index.d.ts +4 -4
  25. package/dist/index.js +50 -15
  26. package/dist/index.js.map +1 -1
  27. package/dist/mailgun.d.cts +2 -2
  28. package/dist/mailgun.d.ts +2 -2
  29. package/dist/newrelic.d.cts +1 -1
  30. package/dist/newrelic.d.ts +1 -1
  31. package/dist/pg-cron.cjs +45 -4
  32. package/dist/pg-cron.cjs.map +1 -1
  33. package/dist/pg-cron.d.cts +5 -2
  34. package/dist/pg-cron.d.ts +5 -2
  35. package/dist/pg-cron.js +45 -4
  36. package/dist/pg-cron.js.map +1 -1
  37. package/dist/postgres.cjs +22 -14
  38. package/dist/postgres.cjs.map +1 -1
  39. package/dist/postgres.d.cts +1 -1
  40. package/dist/postgres.d.ts +1 -1
  41. package/dist/postgres.js +22 -14
  42. package/dist/postgres.js.map +1 -1
  43. package/dist/postmark.d.cts +2 -2
  44. package/dist/postmark.d.ts +2 -2
  45. package/dist/resend.d.cts +2 -2
  46. package/dist/resend.d.ts +2 -2
  47. package/dist/rollbar.d.cts +1 -1
  48. package/dist/rollbar.d.ts +1 -1
  49. package/dist/sendgrid.d.cts +2 -2
  50. package/dist/sendgrid.d.ts +2 -2
  51. package/dist/sentry.d.cts +1 -1
  52. package/dist/sentry.d.ts +1 -1
  53. package/dist/ses.d.cts +2 -2
  54. package/dist/ses.d.ts +2 -2
  55. package/dist/slack.d.cts +1 -1
  56. package/dist/slack.d.ts +1 -1
  57. package/dist/sqlite.cjs +26 -18
  58. package/dist/sqlite.cjs.map +1 -1
  59. package/dist/sqlite.d.cts +1 -1
  60. package/dist/sqlite.d.ts +1 -1
  61. package/dist/sqlite.js +26 -18
  62. package/dist/sqlite.js.map +1 -1
  63. package/dist/twilio.d.cts +1 -1
  64. package/dist/twilio.d.ts +1 -1
  65. package/dist/{types-BjBbCGeJ.d.cts → types-Cz47Boks.d.cts} +6 -0
  66. package/dist/{types-BjBbCGeJ.d.ts → types-Cz47Boks.d.ts} +6 -0
  67. package/dist/webhook.d.cts +1 -1
  68. package/dist/webhook.d.ts +1 -1
  69. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
- import { a as AlertChannel } from './types-BjBbCGeJ.cjs';
2
- import { E as EmailOptions } from './email-DX6gkd5H.cjs';
1
+ import { a as AlertChannel } from './types-Cz47Boks.cjs';
2
+ import { E as EmailOptions } from './email-D1d5niMy.cjs';
3
3
 
4
4
  /**
5
5
  * Mailgun. API reference: https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/messages/post-v3--domain-name--messages
package/dist/mailgun.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as AlertChannel } from './types-BjBbCGeJ.js';
2
- import { E as EmailOptions } from './email-Ciq7GRn4.js';
1
+ import { a as AlertChannel } from './types-Cz47Boks.js';
2
+ import { E as EmailOptions } from './email-BxCp9RQV.js';
3
3
 
4
4
  /**
5
5
  * Mailgun. API reference: https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/messages/post-v3--domain-name--messages
@@ -1,4 +1,4 @@
1
- import { A as Alert, a as AlertChannel } from './types-BjBbCGeJ.cjs';
1
+ import { A as Alert, a as AlertChannel } from './types-Cz47Boks.cjs';
2
2
 
3
3
  /**
4
4
  * New Relic Event API. API reference: https://docs.newrelic.com/docs/data-apis/ingest-apis/event-api/introduction-event-api/
@@ -1,4 +1,4 @@
1
- import { A as Alert, a as AlertChannel } from './types-BjBbCGeJ.js';
1
+ import { A as Alert, a as AlertChannel } from './types-Cz47Boks.js';
2
2
 
3
3
  /**
4
4
  * New Relic Event API. API reference: https://docs.newrelic.com/docs/data-apis/ingest-apis/event-api/introduction-event-api/
package/dist/pg-cron.cjs CHANGED
@@ -82,6 +82,7 @@ function pgCron(db, options = {}) {
82
82
  const retired = /* @__PURE__ */ new Set();
83
83
  let scanned = false;
84
84
  const warned = /* @__PURE__ */ new Set();
85
+ const failing = /* @__PURE__ */ new Set();
85
86
  const warnOnce = (host, key, message) => {
86
87
  if (warned.has(key)) return;
87
88
  warned.add(key);
@@ -135,15 +136,55 @@ function pgCron(db, options = {}) {
135
136
  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.");
136
137
  }
137
138
  const all = rows.map((r) => ({ ...r, jobid: Number(r.jobid) }));
138
- const jobs = all.filter(picks);
139
139
  const names = /* @__PURE__ */ new Map();
140
140
  const definitions = /* @__PURE__ */ new Map();
141
141
  const used = /* @__PURE__ */ new Set();
142
- for (const job of jobs) {
143
- let name = prefix + (options.jobName ? options.jobName(job) : pgCronJobName(job));
142
+ const trouble = (job, what) => {
143
+ if (!failing.has(job.jobid)) {
144
+ failing.add(job.jobid);
145
+ host.onError(new Error(`pg_cron job ${job.jobid}: ${what}; it keeps its last declaration until that works`), "source pg_cron");
146
+ }
147
+ const last = known.get(job.jobid);
148
+ if (!last || used.has(last.name)) return;
149
+ names.set(job.jobid, last.name);
150
+ definitions.set(job.jobid, last.definition);
151
+ used.add(last.name);
152
+ };
153
+ const threw = (e) => e instanceof Error ? `${e.name}: ${e.message}` : String(e);
154
+ for (const job of all) {
155
+ let picked;
156
+ try {
157
+ picked = picks(job);
158
+ } catch (e) {
159
+ trouble(job, `the jobs callback threw ${threw(e)}`);
160
+ continue;
161
+ }
162
+ if (!picked) {
163
+ failing.delete(job.jobid);
164
+ continue;
165
+ }
166
+ let base;
167
+ let extra;
168
+ try {
169
+ base = options.jobName ? options.jobName(job) : pgCronJobName(job);
170
+ } catch (e) {
171
+ trouble(job, `jobName threw ${threw(e)}`);
172
+ continue;
173
+ }
174
+ if (typeof base !== "string") {
175
+ trouble(job, `jobName returned ${base === null ? "null" : typeof base}, not a name`);
176
+ continue;
177
+ }
178
+ try {
179
+ extra = typeof options.options === "function" ? options.options(job) : options.options ?? {};
180
+ } catch (e) {
181
+ trouble(job, `the options callback threw ${threw(e)}`);
182
+ continue;
183
+ }
184
+ failing.delete(job.jobid);
185
+ let name = prefix + base;
144
186
  if (used.has(name)) name = `${name}:${job.jobid}`;
145
187
  used.add(name);
146
- const extra = typeof options.options === "function" ? options.options(job) : options.options ?? {};
147
188
  const schedule = job.active && recording ? pgCronSchedule(job.schedule) : null;
148
189
  let definition = {
149
190
  description: `pg_cron job ${job.jobid} in ${job.database} as ${job.username}${job.active ? "" : " (paused)"}`,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/evaluate.ts","../src/sources/pgcron.ts"],"names":[],"mappings":"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","file":"pg-cron.cjs","sourcesContent":["/**\n * Pure decisions about a job's health. Each function takes the current state\n * and returns the new state plus the alerts that should go out. Nothing here\n * touches a store or a network, which is what makes it testable.\n */\nimport { formatDuration } from \"./duration.js\";\nimport { parseDuration } from \"./duration.js\";\nimport { expectation, nextFire, parseSchedule } from \"./schedule.js\";\nimport { median, percentile } from \"./stats.js\";\nimport type {\n AlertDraft,\n BudgetBreach,\n Condition,\n JobDefinition,\n JobHealth,\n JobState,\n JobSummary,\n Run,\n StoredJob,\n StoredJobDefinition,\n} from \"./types.js\";\n\nexport type { AlertDraft, BudgetBreach };\n\nexport interface Evaluation {\n state: JobState;\n alerts: AlertDraft[];\n}\n\nexport const DEFAULT_GRACE_MS = 10 * 60_000;\nexport const DEFAULT_TIMEOUT_MS = 60 * 60_000;\n/** Runs faster than this are never called slow, whatever the baseline says. */\nexport const SLOW_FLOOR_MS = 10_000;\n/** How many earlier runs a baseline needs before it is trusted. */\nexport const BASELINE_MIN_RUNS = 5;\n/** How many successful runs a baseline looks at, and how many runs a summary covers. */\nexport const BASELINE_WINDOW = 20;\n\n/** The longest duration written: the largest integer JavaScript holds exactly, which every port and store reads back unchanged. */\nexport const MAX_DURATION_MS = Number.MAX_SAFE_INTEGER;\n\n/**\n * How long a run took, from `startedAt` to `finishedAt`: 0 when it started\n * later, and never more than MAX_DURATION_MS. A foreign row's start near a\n * 64-bit limit must not make a duration no store can write.\n */\nexport function runDuration(startedAt: number, finishedAt: number): number {\n const ms = finishedAt - startedAt;\n return ms > 0 ? Math.min(ms, MAX_DURATION_MS) : 0;\n}\n\n/**\n * The version a stored state counts as for compareAndSetState: its\n * `version` when that is a whole number from 0 to MAX_DURATION_MS (2^53 - 1),\n * else 0, as when it is absent. The SQL stores read it the same way, so a\n * foreign row's `1.5`, `\"x\"` or `-1` is written over by the next update\n * instead of refusing every compare-and-set of its job for good.\n */\nexport function stateVersion(state: Pick<JobState, \"version\"> | null | undefined): number {\n const version: unknown = state?.version;\n return typeof version === \"number\" && Number.isSafeInteger(version) && version >= 0 ? version + 0 : 0;\n}\n\nexport function emptyState(job: string): JobState {\n return { job, open: {}, consecutiveFailures: 0, silencedUntil: null, lastAlertAt: null, pendingRecovery: [], undelivered: [] };\n}\n\n/** A stored state with every field present, or a fresh one. State written by an older version lacks the newer fields. */\nexport function normalizeState(state: JobState | null, job: string): JobState {\n if (!state) return emptyState(job);\n return {\n ...emptyState(job),\n ...state,\n open: { ...state.open },\n pendingRecovery: [...(state.pendingRecovery ?? [])],\n undelivered: [...(state.undelivered ?? [])],\n };\n}\n\nfunction cloneState(state: JobState): JobState {\n return normalizeState(state, state.job);\n}\n\nfunction openCondition(state: JobState, condition: Condition, now: number): boolean {\n if (state.open[condition] !== undefined) return false;\n state.open[condition] = now;\n return true;\n}\n\n/**\n * Every open condition has alerted, so closing one owes a recovered message.\n * It is remembered until a successful run leaves nothing open and sends it.\n */\nfunction closeCondition(state: JobState, condition: Condition): boolean {\n if (state.open[condition] === undefined) return false;\n delete state.open[condition];\n const pending = (state.pendingRecovery ??= []);\n if (!pending.includes(condition)) pending.push(condition);\n return true;\n}\n\nexport function openConditions(state: JobState): Condition[] {\n return Object.keys(state.open) as Condition[];\n}\n\nexport function graceMs(def: Pick<JobDefinition, \"grace\">): number {\n return def.grace === undefined ? DEFAULT_GRACE_MS : parseDuration(def.grace, \"grace\");\n}\n\nexport function timeoutMs(def: Pick<JobDefinition, \"timeout\">): number {\n return def.timeout === undefined ? DEFAULT_TIMEOUT_MS : parseDuration(def.timeout, \"timeout\");\n}\n\n/** Slow threshold for a successful run, or null when there is nothing to compare against yet. */\nexport function slowThreshold(def: Pick<JobDefinition, \"maxDuration\">, history: Run[]): { thresholdMs: number; basis: string } | null {\n if (def.maxDuration !== undefined) {\n return { thresholdMs: parseDuration(def.maxDuration, \"maxDuration\"), basis: \"maxDuration\" };\n }\n const durations = history\n .filter((r) => r.status === \"ok\" && r.durationMs !== null)\n .slice(0, BASELINE_WINDOW)\n .map((r) => r.durationMs!);\n if (durations.length < BASELINE_MIN_RUNS) return null;\n const p95 = percentile(durations, 95)!;\n return { thresholdMs: Math.max(2 * p95, SLOW_FLOOR_MS), basis: `twice the p95 of the last ${durations.length} runs (${formatDuration(p95)})` };\n}\n\nexport function budgetBreaches(def: Pick<JobDefinition, \"budget\">, run: Run, history: Run[]): BudgetBreach[] {\n const breaches: BudgetBreach[] = [];\n for (const [metric, value] of Object.entries(run.metrics)) {\n const ceiling = def.budget?.[metric];\n if (ceiling !== undefined) {\n if (value > ceiling) breaches.push({ metric, value, limit: ceiling, basis: \"budget\" });\n continue;\n }\n const past = history\n .filter((r) => r.status === \"ok\" && typeof r.metrics[metric] === \"number\")\n .slice(0, BASELINE_WINDOW)\n .map((r) => r.metrics[metric]!);\n if (past.length < BASELINE_MIN_RUNS) continue;\n const usual = median(past)!;\n if (usual > 0 && value > 3 * usual) {\n breaches.push({ metric, value, limit: 3 * usual, basis: `three times the usual ${formatNumber(usual)}` });\n }\n }\n return breaches;\n}\n\n/** Whether `history` (newest first) holds a full baseline window of successful runs. */\nexport function hasFullBaseline(history: Run[]): boolean {\n return history.filter((r) => r.status === \"ok\").length >= BASELINE_WINDOW;\n}\n\nexport function formatNumber(n: number): string {\n if (Number.isInteger(n)) return n.toLocaleString(\"en-US\");\n return n.toLocaleString(\"en-US\", { maximumFractionDigits: 4 });\n}\n\n/**\n * Called when a run starts. Missed and stuck are about the absence of a run,\n * so a run starting closes them without an alert; the recovered message\n * waits for a successful finish.\n */\nexport function onRunStart(state: JobState): JobState {\n const next = cloneState(state);\n closeCondition(next, \"missed\");\n closeCondition(next, \"stuck\");\n return next;\n}\n\n/**\n * Called when a run finishes with status ok, failed or timeout. `history` is\n * the job's earlier runs, newest first, not including this one.\n */\nexport function onRunFinish(\n def: JobDefinition | StoredJobDefinition,\n run: Run,\n state: JobState,\n history: Run[],\n now: number,\n): Evaluation {\n const next = cloneState(state);\n const alerts: AlertDraft[] = [];\n\n if (run.status === \"ok\") {\n next.consecutiveFailures = 0;\n closeCondition(next, \"missed\");\n closeCondition(next, \"stuck\");\n closeCondition(next, \"failed\");\n\n const slow = slowThreshold(def, history);\n if (slow && run.durationMs !== null && run.durationMs > slow.thresholdMs) {\n if (openCondition(next, \"slow\", now)) {\n alerts.push({ type: \"slow\", run, details: { durationMs: run.durationMs, thresholdMs: slow.thresholdMs, basis: slow.basis } });\n }\n } else {\n closeCondition(next, \"slow\");\n }\n\n const breaches = budgetBreaches(def, run, history);\n if (breaches.length > 0) {\n if (openCondition(next, \"over_budget\", now)) {\n alerts.push({ type: \"over_budget\", run, details: { breaches } });\n }\n } else {\n closeCondition(next, \"over_budget\");\n }\n\n const pending = next.pendingRecovery ?? [];\n if (pending.length > 0 && openConditions(next).length === 0) {\n alerts.push({ type: \"recovered\", run, details: { after: [...pending] } });\n next.pendingRecovery = [];\n }\n return { state: next, alerts };\n }\n\n // failed or timeout\n next.consecutiveFailures += 1;\n closeCondition(next, \"missed\");\n const threshold = Math.max(1, def.failuresBeforeAlert ?? 1);\n const condition = run.status === \"timeout\" ? \"stuck\" : \"failed\";\n if (next.consecutiveFailures >= threshold) {\n if (openCondition(next, condition, now)) {\n alerts.push({ type: condition, run, details: { consecutiveFailures: next.consecutiveFailures, threshold } });\n }\n }\n return { state: next, alerts };\n}\n\n/**\n * Called by check(). Decides whether the schedule has been missed: the run\n * the schedule wants next (see expectation()) has not started and its grace\n * has run out. `lastRun` is the most recent run of any status. A job with no\n * schedule is never missed, and one whose schedule was removed while missed\n * was open gets a recovered alert (reason \"unscheduled\") for missed alone.\n */\nexport function onCheck(\n def: StoredJobDefinition,\n stored: StoredJob,\n lastRun: Run | null,\n state: JobState,\n now: number,\n): Evaluation & { nextExpectedAt: number | null; dueAt: number | null } {\n const next = cloneState(state);\n const alerts: AlertDraft[] = [];\n if (!def.schedule) {\n const since = next.open.missed;\n if (since !== undefined) {\n // The schedule went away while missed was open (the job was declared\n // again without one, or a source retired it), so nothing is due any\n // more. Missed closes now with a recovery of its own; other open\n // conditions keep their own rules. Missed is taken out of the pending\n // recovery too, so the next successful run does not name it again.\n delete next.open.missed;\n next.pendingRecovery = (next.pendingRecovery ?? []).filter((c) => c !== \"missed\");\n alerts.push({ type: \"recovered\", run: lastRun, details: { after: [\"missed\"], reason: \"unscheduled\", since } });\n }\n return { state: next, alerts, nextExpectedAt: null, dueAt: null };\n }\n\n const parsed = parseSchedule(def.schedule, def.timezone);\n const grace = graceMs(def);\n const lastRunAt = lastRun?.startedAt ?? null;\n const exp = expectation(parsed, lastRunAt, stored.createdAt, grace);\n const nextExpectedAt = parsed.kind === \"interval\" ? nextFire(parsed, stored.createdAt, lastRunAt) : nextFire(parsed, now, null);\n if (!exp) return { state: next, alerts, nextExpectedAt, dueAt: null };\n\n // An interval's next run is due a period after the last one started. If that\n // run is still going, the job is busy, not late; stuck covers one that never ends.\n if (parsed.kind === \"interval\" && lastRun?.status === \"running\") {\n return { state: next, alerts, nextExpectedAt, dueAt: exp.dueAt };\n }\n\n if (now > exp.deadline) {\n if (openCondition(next, \"missed\", now)) {\n alerts.push({ type: \"missed\", run: lastRun, details: { dueAt: exp.dueAt, deadline: exp.deadline, graceMs: grace, lastRunAt } });\n }\n } else {\n // A run has started since it opened, or the grace was widened.\n closeCondition(next, \"missed\");\n }\n return { state: next, alerts, nextExpectedAt, dueAt: exp.dueAt };\n}\n\n/** Whether a running run has gone on longer than the job's timeout. */\nexport function isStuck(def: Pick<JobDefinition, \"timeout\">, run: Run, now: number): boolean {\n return run.status === \"running\" && now - run.startedAt > timeoutMs(def);\n}\n\n/**\n * While a job is silenced nothing new is recorded as an incident: conditions\n * may close (so a job that recovered during the silence shows as healthy) but\n * none may open, so the first problem after the silence ends alerts normally.\n */\nexport function muteOpens(previous: JobState, next: JobState): JobState {\n const muted = cloneState(next);\n for (const condition of openConditions(muted)) {\n if (previous.open[condition] === undefined) delete muted.open[condition];\n }\n return muted;\n}\n\nexport function isSilenced(state: JobState, now: number): boolean {\n return state.silencedUntil !== null && state.silencedUntil > now;\n}\n\n/**\n * An evaluation as it is saved and sent: while the job was silenced when it\n * began, nothing opens and nothing is sent.\n */\nexport function applySilence(previous: JobState, evaluation: Evaluation, now: number): Evaluation {\n if (!isSilenced(previous, now)) return evaluation;\n return { state: muteOpens(previous, evaluation.state), alerts: [] };\n}\n\n/**\n * Whether an alert waiting to be retried no longer describes the job, so it\n * is dropped rather than sent late. An alert for a condition is stale once\n * that condition has closed, or has closed and opened again (it opened at a\n * time other than the alert's). A recovery is stale when any condition it\n * names is open again; while they all stay closed it is kept.\n */\nexport function staleAlert(alert: AlertDraft & { at: number }, state: JobState): boolean {\n if (alert.type === \"recovered\") return alert.details.after.some((condition) => state.open[condition] !== undefined);\n return state.open[alert.type] !== alert.at;\n}\n\n/** How a job looks at a glance. Silence wins, then stuck, failing and late. */\nexport function jobHealth(def: Pick<JobDefinition, \"timeout\">, lastRun: Run | null, state: JobState, now: number): JobHealth {\n const open = openConditions(state);\n if (isSilenced(state, now)) return \"silenced\";\n if (open.includes(\"stuck\") || (lastRun && isStuck(def, lastRun, now))) return \"stuck\";\n if (open.includes(\"failed\") || lastRun?.status === \"failed\" || lastRun?.status === \"timeout\") return \"failing\";\n if (open.includes(\"missed\")) return \"late\";\n if (!lastRun) return \"never_ran\";\n return \"healthy\";\n}\n\n/**\n * A job's summary from its most recent runs (newest first; the first\n * BASELINE_WINDOW are used) and its state. Stats cover runs of any status;\n * the percentiles are over the successful ones among them.\n */\nexport function summarize(stored: StoredJob, recent: Run[], state: JobState, nextExpectedAt: number | null, now: number): JobSummary {\n return summary(stored, recent, state, nextExpectedAt, (lastRun) => jobHealth(stored.definition, lastRun, state, now));\n}\n\n/**\n * The summary of a job that could not be evaluated, say because its stored\n * schedule no longer parses. It reads nothing from the definition. The job\n * shows as failing (or silenced, while it is), since it needs a look, and\n * nothing is known about when it is next due.\n */\nexport function unevaluableSummary(stored: StoredJob, recent: Run[], state: JobState, now: number): JobSummary {\n return summary(stored, recent, state, null, () => (isSilenced(state, now) ? \"silenced\" : \"failing\"));\n}\n\nfunction summary(stored: StoredJob, recent: Run[], state: JobState, nextExpectedAt: number | null, health: (lastRun: Run | null) => JobHealth): JobSummary {\n const window = recent.slice(0, BASELINE_WINDOW);\n const lastRun = window[0] ?? null;\n const finished = window.filter((r) => r.status !== \"running\");\n const okDurations = window.filter((r) => r.status === \"ok\" && r.durationMs !== null).map((r) => r.durationMs!);\n return {\n name: stored.name,\n definition: stored.definition,\n health: health(lastRun),\n open: openConditions(state),\n lastRun,\n nextExpectedAt,\n consecutiveFailures: state.consecutiveFailures,\n silencedUntil: state.silencedUntil,\n stats: {\n runs: finished.length,\n okRate: finished.length === 0 ? 1 : finished.filter((r) => r.status === \"ok\").length / finished.length,\n p50Ms: percentile(okDurations, 50),\n p95Ms: percentile(okDurations, 95),\n },\n };\n}\n","import type { Source, SourceHost } from \"../client.js\";\nimport { runDuration } from \"../evaluate.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 : runDuration(startedAt, end),\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"]}
1
+ {"version":3,"sources":["../src/evaluate.ts","../src/sources/pgcron.ts"],"names":[],"mappings":"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","file":"pg-cron.cjs","sourcesContent":["/**\n * Pure decisions about a job's health. Each function takes the current state\n * and returns the new state plus the alerts that should go out. Nothing here\n * touches a store or a network, which is what makes it testable.\n */\nimport { formatDuration } from \"./duration.js\";\nimport { parseDuration } from \"./duration.js\";\nimport { expectation, nextFire, parseSchedule } from \"./schedule.js\";\nimport { median, percentile } from \"./stats.js\";\nimport type {\n AlertDraft,\n BudgetBreach,\n Condition,\n JobDefinition,\n JobHealth,\n JobState,\n JobSummary,\n Run,\n StoredJob,\n StoredJobDefinition,\n} from \"./types.js\";\n\nexport type { AlertDraft, BudgetBreach };\n\nexport interface Evaluation {\n state: JobState;\n alerts: AlertDraft[];\n}\n\nexport const DEFAULT_GRACE_MS = 10 * 60_000;\nexport const DEFAULT_TIMEOUT_MS = 60 * 60_000;\n/** Runs faster than this are never called slow, whatever the baseline says. */\nexport const SLOW_FLOOR_MS = 10_000;\n/** How many earlier runs a baseline needs before it is trusted. */\nexport const BASELINE_MIN_RUNS = 5;\n/** How many successful runs a baseline looks at, and how many runs a summary covers. */\nexport const BASELINE_WINDOW = 20;\n\n/** The longest duration written: the largest integer JavaScript holds exactly, which every port and store reads back unchanged. */\nexport const MAX_DURATION_MS = Number.MAX_SAFE_INTEGER;\n\n/**\n * How long a run took, from `startedAt` to `finishedAt`: 0 when it started\n * later, and never more than MAX_DURATION_MS. A foreign row's start near a\n * 64-bit limit must not make a duration no store can write.\n */\nexport function runDuration(startedAt: number, finishedAt: number): number {\n const ms = finishedAt - startedAt;\n return ms > 0 ? Math.min(ms, MAX_DURATION_MS) : 0;\n}\n\n/**\n * The version a stored state counts as for compareAndSetState: its\n * `version` when that is a whole number from 0 to MAX_DURATION_MS (2^53 - 1),\n * else 0, as when it is absent. The SQL stores read it the same way, so a\n * foreign row's `1.5`, `\"x\"` or `-1` is written over by the next update\n * instead of refusing every compare-and-set of its job for good.\n */\nexport function stateVersion(state: Pick<JobState, \"version\"> | null | undefined): number {\n const version: unknown = state?.version;\n return typeof version === \"number\" && Number.isSafeInteger(version) && version >= 0 ? version + 0 : 0;\n}\n\n/**\n * The failures in a row a stored state counts as: its `consecutiveFailures`\n * when that is a whole number, held at MAX_DURATION_MS (2^53 - 1), and 0 when\n * it is negative or not a whole number. A foreign row's count past 2^53, or at\n * a 64-bit limit, stays at the top instead of losing precision or wrapping\n * negative, and a `1.5`, `\"3\"` or `-1` counts as none.\n */\nexport function failureCount(state: Pick<JobState, \"consecutiveFailures\"> | null | undefined): number {\n const count: unknown = state?.consecutiveFailures;\n if (typeof count !== \"number\" || !Number.isInteger(count) || count <= 0) return 0;\n return Math.min(count, MAX_DURATION_MS);\n}\n\nexport function emptyState(job: string): JobState {\n return { job, open: {}, consecutiveFailures: 0, silencedUntil: null, lastAlertAt: null, pendingRecovery: [], undelivered: [] };\n}\n\n/** A stored state with every field present, or a fresh one. State written by an older version lacks the newer fields. */\nexport function normalizeState(state: JobState | null, job: string): JobState {\n if (!state) return emptyState(job);\n return {\n ...emptyState(job),\n ...state,\n open: { ...state.open },\n consecutiveFailures: failureCount(state),\n pendingRecovery: [...(state.pendingRecovery ?? [])],\n undelivered: [...(state.undelivered ?? [])],\n };\n}\n\nfunction cloneState(state: JobState): JobState {\n return normalizeState(state, state.job);\n}\n\nfunction openCondition(state: JobState, condition: Condition, now: number): boolean {\n if (state.open[condition] !== undefined) return false;\n state.open[condition] = now;\n return true;\n}\n\n/**\n * Every open condition has alerted, so closing one owes a recovered message.\n * It is remembered until a successful run leaves nothing open and sends it.\n */\nfunction closeCondition(state: JobState, condition: Condition): boolean {\n if (state.open[condition] === undefined) return false;\n delete state.open[condition];\n const pending = (state.pendingRecovery ??= []);\n if (!pending.includes(condition)) pending.push(condition);\n return true;\n}\n\nexport function openConditions(state: JobState): Condition[] {\n return Object.keys(state.open) as Condition[];\n}\n\nexport function graceMs(def: Pick<JobDefinition, \"grace\">): number {\n return def.grace === undefined ? DEFAULT_GRACE_MS : parseDuration(def.grace, \"grace\");\n}\n\nexport function timeoutMs(def: Pick<JobDefinition, \"timeout\">): number {\n return def.timeout === undefined ? DEFAULT_TIMEOUT_MS : parseDuration(def.timeout, \"timeout\");\n}\n\n/** Slow threshold for a successful run, or null when there is nothing to compare against yet. */\nexport function slowThreshold(def: Pick<JobDefinition, \"maxDuration\">, history: Run[]): { thresholdMs: number; basis: string } | null {\n if (def.maxDuration !== undefined) {\n return { thresholdMs: parseDuration(def.maxDuration, \"maxDuration\"), basis: \"maxDuration\" };\n }\n const durations = history\n .filter((r) => r.status === \"ok\" && r.durationMs !== null)\n .slice(0, BASELINE_WINDOW)\n .map((r) => r.durationMs!);\n if (durations.length < BASELINE_MIN_RUNS) return null;\n const p95 = percentile(durations, 95)!;\n return { thresholdMs: Math.max(2 * p95, SLOW_FLOOR_MS), basis: `twice the p95 of the last ${durations.length} runs (${formatDuration(p95)})` };\n}\n\nexport function budgetBreaches(def: Pick<JobDefinition, \"budget\">, run: Run, history: Run[]): BudgetBreach[] {\n const breaches: BudgetBreach[] = [];\n for (const [metric, value] of Object.entries(run.metrics)) {\n const ceiling = def.budget?.[metric];\n if (ceiling !== undefined) {\n if (value > ceiling) breaches.push({ metric, value, limit: ceiling, basis: \"budget\" });\n continue;\n }\n const past = history\n .filter((r) => r.status === \"ok\" && typeof r.metrics[metric] === \"number\")\n .slice(0, BASELINE_WINDOW)\n .map((r) => r.metrics[metric]!);\n if (past.length < BASELINE_MIN_RUNS) continue;\n const usual = median(past)!;\n if (usual > 0 && value > 3 * usual) {\n breaches.push({ metric, value, limit: 3 * usual, basis: `three times the usual ${formatNumber(usual)}` });\n }\n }\n return breaches;\n}\n\n/** Whether `history` (newest first) holds a full baseline window of successful runs. */\nexport function hasFullBaseline(history: Run[]): boolean {\n return history.filter((r) => r.status === \"ok\").length >= BASELINE_WINDOW;\n}\n\nexport function formatNumber(n: number): string {\n if (Number.isInteger(n)) return n.toLocaleString(\"en-US\");\n return n.toLocaleString(\"en-US\", { maximumFractionDigits: 4 });\n}\n\n/**\n * Called when a run starts. Missed and stuck are about the absence of a run,\n * so a run starting closes them without an alert; the recovered message\n * waits for a successful finish.\n */\nexport function onRunStart(state: JobState): JobState {\n const next = cloneState(state);\n closeCondition(next, \"missed\");\n closeCondition(next, \"stuck\");\n return next;\n}\n\n/**\n * Called when a run finishes with status ok, failed or timeout. `history` is\n * the job's earlier runs, newest first, not including this one.\n */\nexport function onRunFinish(\n def: JobDefinition | StoredJobDefinition,\n run: Run,\n state: JobState,\n history: Run[],\n now: number,\n): Evaluation {\n const next = cloneState(state);\n const alerts: AlertDraft[] = [];\n\n if (run.status === \"ok\") {\n next.consecutiveFailures = 0;\n closeCondition(next, \"missed\");\n closeCondition(next, \"stuck\");\n closeCondition(next, \"failed\");\n\n const slow = slowThreshold(def, history);\n if (slow && run.durationMs !== null && run.durationMs > slow.thresholdMs) {\n if (openCondition(next, \"slow\", now)) {\n alerts.push({ type: \"slow\", run, details: { durationMs: run.durationMs, thresholdMs: slow.thresholdMs, basis: slow.basis } });\n }\n } else {\n closeCondition(next, \"slow\");\n }\n\n const breaches = budgetBreaches(def, run, history);\n if (breaches.length > 0) {\n if (openCondition(next, \"over_budget\", now)) {\n alerts.push({ type: \"over_budget\", run, details: { breaches } });\n }\n } else {\n closeCondition(next, \"over_budget\");\n }\n\n const pending = next.pendingRecovery ?? [];\n if (pending.length > 0 && openConditions(next).length === 0) {\n alerts.push({ type: \"recovered\", run, details: { after: [...pending] } });\n next.pendingRecovery = [];\n }\n return { state: next, alerts };\n }\n\n // failed or timeout\n // Held at the top: a count at the limit neither loses precision nor wraps below a threshold.\n next.consecutiveFailures = Math.min(next.consecutiveFailures + 1, MAX_DURATION_MS);\n closeCondition(next, \"missed\");\n const threshold = Math.max(1, def.failuresBeforeAlert ?? 1);\n const condition = run.status === \"timeout\" ? \"stuck\" : \"failed\";\n if (next.consecutiveFailures >= threshold) {\n if (openCondition(next, condition, now)) {\n alerts.push({ type: condition, run, details: { consecutiveFailures: next.consecutiveFailures, threshold } });\n }\n }\n return { state: next, alerts };\n}\n\n/**\n * Called by check(). Decides whether the schedule has been missed: the run\n * the schedule wants next (see expectation()) has not started and its grace\n * has run out. `lastRun` is the most recent run of any status. A job with no\n * schedule is never missed, and one whose schedule was removed while missed\n * was open gets a recovered alert (reason \"unscheduled\") for missed alone.\n */\nexport function onCheck(\n def: StoredJobDefinition,\n stored: StoredJob,\n lastRun: Run | null,\n state: JobState,\n now: number,\n): Evaluation & { nextExpectedAt: number | null; dueAt: number | null } {\n const next = cloneState(state);\n const alerts: AlertDraft[] = [];\n if (!def.schedule) {\n const since = next.open.missed;\n if (since !== undefined) {\n // The schedule went away while missed was open (the job was declared\n // again without one, or a source retired it), so nothing is due any\n // more. Missed closes now with a recovery of its own; other open\n // conditions keep their own rules. Missed is taken out of the pending\n // recovery too, so the next successful run does not name it again.\n delete next.open.missed;\n next.pendingRecovery = (next.pendingRecovery ?? []).filter((c) => c !== \"missed\");\n alerts.push({ type: \"recovered\", run: lastRun, details: { after: [\"missed\"], reason: \"unscheduled\", since } });\n }\n return { state: next, alerts, nextExpectedAt: null, dueAt: null };\n }\n\n const parsed = parseSchedule(def.schedule, def.timezone);\n const grace = graceMs(def);\n const lastRunAt = lastRun?.startedAt ?? null;\n const exp = expectation(parsed, lastRunAt, stored.createdAt, grace);\n const nextExpectedAt = parsed.kind === \"interval\" ? nextFire(parsed, stored.createdAt, lastRunAt) : nextFire(parsed, now, null);\n if (!exp) return { state: next, alerts, nextExpectedAt, dueAt: null };\n\n // An interval's next run is due a period after the last one started. If that\n // run is still going, the job is busy, not late; stuck covers one that never ends.\n if (parsed.kind === \"interval\" && lastRun?.status === \"running\") {\n return { state: next, alerts, nextExpectedAt, dueAt: exp.dueAt };\n }\n\n if (now > exp.deadline) {\n if (openCondition(next, \"missed\", now)) {\n alerts.push({ type: \"missed\", run: lastRun, details: { dueAt: exp.dueAt, deadline: exp.deadline, graceMs: grace, lastRunAt } });\n }\n } else {\n // A run has started since it opened, or the grace was widened.\n closeCondition(next, \"missed\");\n }\n return { state: next, alerts, nextExpectedAt, dueAt: exp.dueAt };\n}\n\n/** Whether a running run has gone on longer than the job's timeout. */\nexport function isStuck(def: Pick<JobDefinition, \"timeout\">, run: Run, now: number): boolean {\n return run.status === \"running\" && now - run.startedAt > timeoutMs(def);\n}\n\n/**\n * While a job is silenced nothing new is recorded as an incident: conditions\n * may close (so a job that recovered during the silence shows as healthy) but\n * none may open, so the first problem after the silence ends alerts normally.\n */\nexport function muteOpens(previous: JobState, next: JobState): JobState {\n const muted = cloneState(next);\n for (const condition of openConditions(muted)) {\n if (previous.open[condition] === undefined) delete muted.open[condition];\n }\n return muted;\n}\n\nexport function isSilenced(state: JobState, now: number): boolean {\n return state.silencedUntil !== null && state.silencedUntil > now;\n}\n\n/**\n * An evaluation as it is saved and sent: while the job was silenced when it\n * began, nothing opens and nothing is sent.\n */\nexport function applySilence(previous: JobState, evaluation: Evaluation, now: number): Evaluation {\n if (!isSilenced(previous, now)) return evaluation;\n return { state: muteOpens(previous, evaluation.state), alerts: [] };\n}\n\n/**\n * Whether an alert waiting to be retried no longer describes the job, so it\n * is dropped rather than sent late. An alert for a condition is stale once\n * that condition has closed, or has closed and opened again (it opened at a\n * time other than the alert's). A recovery is stale when any condition it\n * names is open again; while they all stay closed it is kept.\n */\nexport function staleAlert(alert: AlertDraft & { at: number }, state: JobState): boolean {\n if (alert.type === \"recovered\") return alert.details.after.some((condition) => state.open[condition] !== undefined);\n return state.open[alert.type] !== alert.at;\n}\n\n/** How a job looks at a glance. Silence wins, then stuck, failing and late. */\nexport function jobHealth(def: Pick<JobDefinition, \"timeout\">, lastRun: Run | null, state: JobState, now: number): JobHealth {\n const open = openConditions(state);\n if (isSilenced(state, now)) return \"silenced\";\n if (open.includes(\"stuck\") || (lastRun && isStuck(def, lastRun, now))) return \"stuck\";\n if (open.includes(\"failed\") || lastRun?.status === \"failed\" || lastRun?.status === \"timeout\") return \"failing\";\n if (open.includes(\"missed\")) return \"late\";\n if (!lastRun) return \"never_ran\";\n return \"healthy\";\n}\n\n/**\n * A job's summary from its most recent runs (newest first; the first\n * BASELINE_WINDOW are used) and its state. Stats cover runs of any status;\n * the percentiles are over the successful ones among them.\n */\nexport function summarize(stored: StoredJob, recent: Run[], state: JobState, nextExpectedAt: number | null, now: number): JobSummary {\n return summary(stored, recent, state, nextExpectedAt, (lastRun) => jobHealth(stored.definition, lastRun, state, now));\n}\n\n/**\n * The summary of a job that could not be evaluated, say because its stored\n * schedule no longer parses. It reads nothing from the definition. The job\n * shows as failing (or silenced, while it is), since it needs a look, and\n * nothing is known about when it is next due.\n */\nexport function unevaluableSummary(stored: StoredJob, recent: Run[], state: JobState, now: number): JobSummary {\n return summary(stored, recent, state, null, () => (isSilenced(state, now) ? \"silenced\" : \"failing\"));\n}\n\nfunction summary(stored: StoredJob, recent: Run[], state: JobState, nextExpectedAt: number | null, health: (lastRun: Run | null) => JobHealth): JobSummary {\n const window = recent.slice(0, BASELINE_WINDOW);\n const lastRun = window[0] ?? null;\n const finished = window.filter((r) => r.status !== \"running\");\n const okDurations = window.filter((r) => r.status === \"ok\" && r.durationMs !== null).map((r) => r.durationMs!);\n return {\n name: stored.name,\n definition: stored.definition,\n health: health(lastRun),\n open: openConditions(state),\n lastRun,\n nextExpectedAt,\n consecutiveFailures: state.consecutiveFailures,\n silencedUntil: state.silencedUntil,\n stats: {\n runs: finished.length,\n okRate: finished.length === 0 ? 1 : finished.filter((r) => r.status === \"ok\").length / finished.length,\n p50Ms: percentile(okDurations, 50),\n p95Ms: percentile(okDurations, 95),\n },\n };\n}\n","import type { Source, SourceHost } from \"../client.js\";\nimport { runDuration } from \"../evaluate.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 * One that throws or returns no string, like a `jobs` or `options`\n * function that throws, is reported once and fails only that job, which\n * keeps its last declaration until the callback works again.\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 : runDuration(startedAt, end),\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 /** Jobids whose callback failed, reported once until it works again. */\n const failing = new Set<number>();\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\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 /**\n * A callback of the app's (jobs, jobName, options) that threw, or a\n * jobName that gave no name, fails only its job, as a bad row does:\n * reported once until it works again, and the job carries on as last\n * declared (skipped when it never was), so its runs are still copied.\n */\n const trouble = (job: PgCronJob, what: string) => {\n if (!failing.has(job.jobid)) {\n failing.add(job.jobid);\n host.onError(new Error(`pg_cron job ${job.jobid}: ${what}; it keeps its last declaration until that works`), \"source pg_cron\");\n }\n const last = known.get(job.jobid);\n if (!last || used.has(last.name)) return;\n names.set(job.jobid, last.name);\n definitions.set(job.jobid, last.definition);\n used.add(last.name);\n };\n const threw = (e: unknown) => (e instanceof Error ? `${e.name}: ${e.message}` : String(e));\n for (const job of all) {\n let picked: boolean;\n try {\n picked = picks(job);\n } catch (e) {\n trouble(job, `the jobs callback threw ${threw(e)}`);\n continue;\n }\n if (!picked) {\n failing.delete(job.jobid);\n continue;\n }\n let base: unknown;\n let extra: Omit<JobOptions, \"schedule\" | \"timezone\">;\n try {\n base = options.jobName ? options.jobName(job) : pgCronJobName(job);\n } catch (e) {\n trouble(job, `jobName threw ${threw(e)}`);\n continue;\n }\n if (typeof base !== \"string\") {\n trouble(job, `jobName returned ${base === null ? \"null\" : typeof base}, not a name`);\n continue;\n }\n try {\n extra = typeof options.options === \"function\" ? options.options(job) : (options.options ?? {});\n } catch (e) {\n trouble(job, `the options callback threw ${threw(e)}`);\n continue;\n }\n failing.delete(job.jobid);\n let name = prefix + base;\n if (used.has(name)) name = `${name}:${job.jobid}`;\n used.add(name);\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"]}
@@ -1,5 +1,5 @@
1
- import { S as Source } from './client-Di5HAWQz.cjs';
2
- import { i as JobOptions, R as Run } from './types-BjBbCGeJ.cjs';
1
+ import { S as Source } from './client-D35cfnNV.cjs';
2
+ import { i as JobOptions, R as Run } from './types-Cz47Boks.cjs';
3
3
 
4
4
  /**
5
5
  * Anything with a pg-style query(): a `pg` Pool or Client, or a wrapper
@@ -31,6 +31,9 @@ interface PgCronOptions {
31
31
  * The CronWatch name for a job. Default its jobname with anything other
32
32
  * than letters, digits, ".", "_", ":" and "-" turned into "-", or
33
33
  * "pg_cron:<jobid>" when it has none. The prefix goes in front either way.
34
+ * One that throws or returns no string, like a `jobs` or `options`
35
+ * function that throws, is reported once and fails only that job, which
36
+ * keeps its last declaration until the callback works again.
34
37
  */
35
38
  jobName?: (job: PgCronJob) => string;
36
39
  /** Grace, timeout, maxDuration, expect and the rest, for every job or per job. The schedule always comes from pg_cron. */
package/dist/pg-cron.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as Source } from './client-B5hM4l-D.js';
2
- import { i as JobOptions, R as Run } from './types-BjBbCGeJ.js';
1
+ import { S as Source } from './client-CQm-8-mR.js';
2
+ import { i as JobOptions, R as Run } from './types-Cz47Boks.js';
3
3
 
4
4
  /**
5
5
  * Anything with a pg-style query(): a `pg` Pool or Client, or a wrapper
@@ -31,6 +31,9 @@ interface PgCronOptions {
31
31
  * The CronWatch name for a job. Default its jobname with anything other
32
32
  * than letters, digits, ".", "_", ":" and "-" turned into "-", or
33
33
  * "pg_cron:<jobid>" when it has none. The prefix goes in front either way.
34
+ * One that throws or returns no string, like a `jobs` or `options`
35
+ * function that throws, is reported once and fails only that job, which
36
+ * keeps its last declaration until the callback works again.
34
37
  */
35
38
  jobName?: (job: PgCronJob) => string;
36
39
  /** Grace, timeout, maxDuration, expect and the rest, for every job or per job. The schedule always comes from pg_cron. */
package/dist/pg-cron.js CHANGED
@@ -80,6 +80,7 @@ function pgCron(db, options = {}) {
80
80
  const retired = /* @__PURE__ */ new Set();
81
81
  let scanned = false;
82
82
  const warned = /* @__PURE__ */ new Set();
83
+ const failing = /* @__PURE__ */ new Set();
83
84
  const warnOnce = (host, key, message) => {
84
85
  if (warned.has(key)) return;
85
86
  warned.add(key);
@@ -133,15 +134,55 @@ function pgCron(db, options = {}) {
133
134
  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.");
134
135
  }
135
136
  const all = rows.map((r) => ({ ...r, jobid: Number(r.jobid) }));
136
- const jobs = all.filter(picks);
137
137
  const names = /* @__PURE__ */ new Map();
138
138
  const definitions = /* @__PURE__ */ new Map();
139
139
  const used = /* @__PURE__ */ new Set();
140
- for (const job of jobs) {
141
- let name = prefix + (options.jobName ? options.jobName(job) : pgCronJobName(job));
140
+ const trouble = (job, what) => {
141
+ if (!failing.has(job.jobid)) {
142
+ failing.add(job.jobid);
143
+ host.onError(new Error(`pg_cron job ${job.jobid}: ${what}; it keeps its last declaration until that works`), "source pg_cron");
144
+ }
145
+ const last = known.get(job.jobid);
146
+ if (!last || used.has(last.name)) return;
147
+ names.set(job.jobid, last.name);
148
+ definitions.set(job.jobid, last.definition);
149
+ used.add(last.name);
150
+ };
151
+ const threw = (e) => e instanceof Error ? `${e.name}: ${e.message}` : String(e);
152
+ for (const job of all) {
153
+ let picked;
154
+ try {
155
+ picked = picks(job);
156
+ } catch (e) {
157
+ trouble(job, `the jobs callback threw ${threw(e)}`);
158
+ continue;
159
+ }
160
+ if (!picked) {
161
+ failing.delete(job.jobid);
162
+ continue;
163
+ }
164
+ let base;
165
+ let extra;
166
+ try {
167
+ base = options.jobName ? options.jobName(job) : pgCronJobName(job);
168
+ } catch (e) {
169
+ trouble(job, `jobName threw ${threw(e)}`);
170
+ continue;
171
+ }
172
+ if (typeof base !== "string") {
173
+ trouble(job, `jobName returned ${base === null ? "null" : typeof base}, not a name`);
174
+ continue;
175
+ }
176
+ try {
177
+ extra = typeof options.options === "function" ? options.options(job) : options.options ?? {};
178
+ } catch (e) {
179
+ trouble(job, `the options callback threw ${threw(e)}`);
180
+ continue;
181
+ }
182
+ failing.delete(job.jobid);
183
+ let name = prefix + base;
142
184
  if (used.has(name)) name = `${name}:${job.jobid}`;
143
185
  used.add(name);
144
- const extra = typeof options.options === "function" ? options.options(job) : options.options ?? {};
145
186
  const schedule = job.active && recording ? pgCronSchedule(job.schedule) : null;
146
187
  let definition = {
147
188
  description: `pg_cron job ${job.jobid} in ${job.database} as ${job.username}${job.active ? "" : " (paused)"}`,