@cronwatch/sdk 0.14.3 → 0.15.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/evaluate.ts","../src/sources/pgcron.ts","../src/entries/pg-cron.ts"],"names":["PG_CRON_HOLD_MS","pgCronSchedule","pgCronJobName","pgCronRun"],"mappings":"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","file":"pg-cron.js","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 Alert,\n AlertDraft,\n BudgetBreach,\n Condition,\n HeldAlert,\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 * When a silence of `ms` from `now` ends: a whole millisecond, never past\n * MAX_DURATION_MS (2^53 - 1), however long the silence asked for. Every port\n * sharing the store reads it back unchanged, where a larger number could\n * wrap to a time long past and send alerts during the silence.\n */\nexport function silenceEnd(now: number, ms: number): number {\n return Math.min(now + Math.floor(Math.min(ms, MAX_DURATION_MS)), MAX_DURATION_MS);\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 JSON object: not null, not an array. */\nexport function isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * A stored state with every field present, or a fresh one. State written by\n * an older version lacks the newer fields. `sending` and `underFloor` are the\n * exceptions: each is there only while it holds something.\n *\n * Read leniently, since a foreign, hand-edited, or damaged row must affect\n * only its own job, and the next write puts it right: a state that is not\n * an object reads as none; `open` keeps only its entries whose value is a\n * number (anything but an object reads as {}); `silencedUntil` and\n * `lastAlertAt` that are not numbers read as null; `pendingRecovery` keeps\n * only its strings, and `undelivered` only its entries that are objects (a\n * list of neither shape reads as []), and `underFloor` and `pendingRuns`\n * only their strings (each absent when none). Unknown fields are kept as\n * written.\n */\nexport function normalizeState(state: JobState | null, job: string): JobState {\n if (!isObject(state)) return emptyState(job);\n const { sending, underFloor, pendingRuns, ...rest } = state;\n const open = isObject(state.open)\n ? Object.fromEntries(Object.entries(state.open).filter(([, at]) => typeof at === \"number\"))\n : {};\n const number = (value: unknown) => (typeof value === \"number\" ? value : null);\n const list = (value: unknown) => (Array.isArray(value) ? value : []);\n return {\n ...emptyState(job),\n ...rest,\n open,\n consecutiveFailures: failureCount(state),\n silencedUntil: number(state.silencedUntil),\n lastAlertAt: number(state.lastAlertAt),\n pendingRecovery: list(state.pendingRecovery).filter((c): c is Condition => typeof c === \"string\"),\n undelivered: list(state.undelivered).filter((alert): alert is Alert => isObject(alert)),\n ...(Array.isArray(sending) && sending.length > 0 ? { sending: [...sending] } : {}),\n ...(Array.isArray(underFloor) && underFloor.some((m) => typeof m === \"string\") ? { underFloor: underFloor.filter((m): m is string => typeof m === \"string\") } : {}),\n ...(Array.isArray(pendingRuns) && pendingRuns.some((id) => typeof id === \"string\") ? { pendingRuns: pendingRuns.filter((id): id is string => typeof id === \"string\") } : {}),\n };\n}\n\n// ---------------------------------------------------------------- delivery\n\n/** Alerts kept per job for retry, and per job being sent; past it the oldest go. */\nexport const MAX_UNDELIVERED = 20;\n\n/**\n * How long an alert in `sending` is left to the process sending it. Longer\n * than any send takes: at most three alerts go out together, each with 25\n * seconds of triage and 15 of channels.\n */\nexport const SEND_LEASE_MS = 5 * 60_000;\n\n/** Identifies an alert across retries, and in `sending`. */\nexport function alertKey(alert: Pick<Alert, \"type\" | \"at\" | \"run\">): string {\n return `${alert.type}|${alert.at}|${alert.run?.id ?? \"\"}`;\n}\n\n/** How much of a held alert's run error and output is kept: 2048 UTF-16 units of each. */\nexport const HELD_TEXT_CAP = 2048;\n\n/**\n * How far past HELD_TEXT_CAP a text may run and still be kept whole: longer\n * than any marker, so a text trimmed once (its kept part and its marker) is\n * never trimmed again, and the count in its marker stays true.\n */\nexport const HELD_TEXT_SLACK = 64;\n\n/**\n * A run as an alert held in a job's state keeps it (`sending` and\n * `undelivered`), so the state stays small however much a run logged. Its\n * error keeps its first HELD_TEXT_CAP units and its output its last, each\n * cut so as not to split a surrogate pair, with a line saying how many\n * units were left out: \"<head>\\n[N more characters trimmed]\" and\n * \"[N earlier characters trimmed]\\n<tail>\". A text of at most\n * HELD_TEXT_CAP + HELD_TEXT_SLACK units, or that is not a string, is kept\n * as it is, and so is every other field. The first send of an alert uses\n * the alert as composed; only a retry reads the held copy.\n */\nexport function heldRun(run: Run): Run {\n const error = typeof run.error === \"string\" && run.error.length > HELD_TEXT_CAP + HELD_TEXT_SLACK ? keepHead(run.error) : run.error;\n const output = typeof run.output === \"string\" && run.output.length > HELD_TEXT_CAP + HELD_TEXT_SLACK ? keepTail(run.output) : run.output;\n return error === run.error && output === run.output ? run : { ...run, error, output };\n}\n\n/** How much of a held alert's message is kept: its first 4096 UTF-16 units. */\nexport const HELD_MESSAGE_CAP = 4096;\n\nfunction keepHead(text: string, cap = HELD_TEXT_CAP): string {\n let end = cap;\n if (isHighSurrogate(text.charCodeAt(end - 1))) end -= 1;\n return `${text.slice(0, end)}\\n[${text.length - end} more characters trimmed]`;\n}\n\nfunction keepTail(text: string): string {\n let start = text.length - HELD_TEXT_CAP;\n if (isLowSurrogate(text.charCodeAt(start)) && isHighSurrogate(text.charCodeAt(start - 1))) start += 1;\n return `[${start} earlier characters trimmed]\\n${text.slice(start)}`;\n}\n\nfunction isHighSurrogate(code: number): boolean {\n return code >= 0xd800 && code <= 0xdbff;\n}\n\nfunction isLowSurrogate(code: number): boolean {\n return code >= 0xdc00 && code <= 0xdfff;\n}\n\n/**\n * An alert as it is held in state: its run trimmed (heldRun), and its\n * message, when longer than HELD_MESSAGE_CAP + HELD_TEXT_SLACK units, cut to\n * its first HELD_MESSAGE_CAP like a run's error (\"<head>\\n[N more characters\n * trimmed]\"). Everything else, the title included, as it is. The first send\n * uses the alert as composed.\n */\nexport function heldAlert<T extends HeldAlert>(alert: T): T {\n const run: unknown = alert.run;\n const trimmedRun = isObject(run) ? heldRun(run as unknown as Run) : run;\n const message: unknown = alert.message;\n const trimmedMessage = typeof message === \"string\" && message.length > HELD_MESSAGE_CAP + HELD_TEXT_SLACK ? keepHead(message, HELD_MESSAGE_CAP) : message;\n if (trimmedRun === run && trimmedMessage === message) return alert;\n return { ...alert, run: trimmedRun, message: trimmedMessage } as T;\n}\n\n/**\n * Each configured channel's key, in the order configured: its name, with\n * \"#2\", \"#3\", and so on for the second and later channels of the same name,\n * so two webhooks are \"webhook\" and \"webhook#2\". A key already given to an\n * earlier channel (a channel named \"webhook#2\" itself, say) is skipped for\n * the next number up, so every key is different. A held alert names the\n * channels that still owe it by these keys (HeldAlert.owed).\n */\nexport function channelKeys(names: readonly string[]): string[] {\n const seen = new Map<string, number>();\n const taken = new Set<string>();\n return names.map((name) => {\n let n = (seen.get(name) ?? 0) + 1;\n seen.set(name, n);\n let key = n === 1 ? name : `${name}#${n}`;\n while (taken.has(key)) key = `${name}#${++n}`;\n taken.add(key);\n return key;\n });\n}\n\n/**\n * The channels a held alert is still owed by, of those configured now\n * (`configured`, channelKeys order): those in its `owed` that are still\n * configured, or every configured one when it has no `owed` list (an alert\n * held by a release before per-channel delivery, or one never sent). An\n * entry of `owed` that is not a string is ignored. None left means the\n * alert is done.\n */\nexport function owedChannels(alert: HeldAlert, configured: readonly string[]): string[] {\n const owed: unknown = alert.owed;\n if (!Array.isArray(owed)) return [...configured];\n return configured.filter((key) => owed.includes(key));\n}\n\n/**\n * An alert after a send to the channels in `tried`, of which those in\n * `failed` refused it: null when none is left owing it, else the alert\n * with `owed` set to those that refused, in configured order.\n */\nexport function afterSend<T extends HeldAlert>(alert: T, tried: readonly string[], failed: readonly string[]): T | null {\n const owed = tried.filter((key) => failed.includes(key));\n return owed.length === 0 ? null : { ...alert, owed };\n}\n\n/** An alert without its `owed` list, as a channel is given it. */\nexport function withoutOwed(alert: HeldAlert): Alert {\n if (!(\"owed\" in alert)) return alert;\n const { owed: _owed, ...rest } = alert;\n return rest as Alert;\n}\n\n/** Keeps the newest `max` of `list`, and says how many went. */\nfunction newest<T>(list: T[], max: number): { kept: T[]; dropped: number } {\n return { kept: list.slice(-max), dropped: Math.max(0, list.length - max) };\n}\n\n/**\n * `alerts` added to the undelivered queue, each with its run trimmed\n * (heldAlert) and its `owed` list as given: one with the same key as a\n * queued alert replaces it where it stands, the rest go at the end, and only\n * the newest MAX_UNDELIVERED stay. `dropped` counts those let go.\n */\nexport function queueUndelivered(state: JobState, alerts: HeldAlert[]): { state: JobState; dropped: number } {\n const next = cloneState(state);\n const byKey = new Map(alerts.map((alert) => [alertKey(alert), heldAlert(alert)]));\n const queue = next.undelivered!.map((alert) => byKey.get(alertKey(alert)) ?? alert);\n const known = new Set(queue.map(alertKey));\n queue.push(...[...byKey.values()].filter((alert) => !known.has(alertKey(alert))));\n const { kept, dropped } = newest(queue, MAX_UNDELIVERED);\n next.undelivered = kept;\n return { state: next, dropped };\n}\n\n/**\n * The outbox. Alerts just composed are written with the state that opens\n * their condition, before any is sent, so a process that stops part way does\n * not lose them: into `sending`, each with its lease ending at `until`, when\n * this process sends them, or (deliver: \"check\") straight into the\n * undelivered queue for a check elsewhere. Either way the copy held has its\n * run trimmed (heldAlert) and no `owed` list, so every configured channel\n * owes it; the first send uses the alerts as composed. `dropped` counts\n * alerts let go past MAX_UNDELIVERED.\n */\nexport function holdAlerts(state: JobState, alerts: Alert[], until: number, deferred: boolean): { state: JobState; dropped: number } {\n if (alerts.length === 0) return { state, dropped: 0 };\n if (deferred) return queueUndelivered(state, alerts);\n const next = cloneState(state);\n const { kept, dropped } = newest([...(next.sending ?? []), ...alerts.map((alert) => ({ until, alert: heldAlert(alert) }))], MAX_UNDELIVERED);\n next.sending = kept;\n return { state: next, dropped };\n}\n\n/**\n * Alerts in `sending` whose lease ran out by `now`: the process sending them\n * stopped before it recorded how the send went. They go to the undelivered\n * queue, where the retry sends them (with triage, which is never stored with\n * them here) or drops them as stale, owed by the channels its `owed` names.\n * An entry that is not an object with an alert is dropped; one without a\n * numeric `until` counts as run out.\n */\nexport function releaseSending(state: JobState, now: number): { state: JobState; dropped: number } {\n const sending = state.sending ?? [];\n const lapsed = sending.filter((entry) => !(typeof entry?.until === \"number\" && entry.until > now));\n if (lapsed.length === 0) return { state, dropped: 0 };\n const held = sending.filter((entry) => !lapsed.includes(entry));\n return queueUndelivered(\n { ...state, sending: held },\n lapsed.filter((entry) => typeof entry?.alert === \"object\" && entry.alert !== null).map((entry) => entry.alert),\n );\n}\n\n/**\n * How a send went. Delivered alerts (no channel owes them any more) and\n * stale ones (`dropped`) leave the queue; failed ones (some channel still\n * owes them, named by their `owed`) replace their queued copy, so a triage\n * made on this attempt and the shrunken `owed` are kept, or join the queue.\n * Every one of them leaves `sending`. lastAlertAt moves to `now` when\n * `reached` (some channel accepted an alert on this attempt; by default,\n * when any was delivered). `dropped` in the result counts alerts let go\n * past MAX_UNDELIVERED.\n */\nexport function recordSent(\n state: JobState,\n delivered: HeldAlert[],\n failed: HeldAlert[],\n stale: HeldAlert[],\n now: number,\n reached: boolean = delivered.length > 0,\n): { state: JobState; dropped: number } {\n const next = cloneState(state);\n const done = new Set([...delivered, ...stale].map(alertKey));\n next.undelivered = next.undelivered!.filter((alert) => !done.has(alertKey(alert)));\n const sent = new Set([...delivered, ...failed, ...stale].map(alertKey));\n const held = (next.sending ?? []).filter((entry) => !(typeof entry?.alert === \"object\" && entry.alert !== null && sent.has(alertKey(entry.alert))));\n if (held.length > 0) next.sending = held;\n else delete next.sending;\n if (reached) next.lastAlertAt = now;\n return queueUndelivered(next, failed);\n}\n\n/**\n * A check's claim on the undelivered queue, written before it sends, so two\n * processes checking at once never retry the same alert: the queued alerts\n * that still describe the job move into `sending`, oldest first, each with\n * its lease ending at `until`, as many as `sending` has room for under\n * MAX_UNDELIVERED (the rest wait for the next check); stale ones\n * (staleAlert) leave the queue. While the job is silenced only recoveries\n * are claimed. The process whose write lands sends `claimed`.\n */\nexport function claimUndelivered(state: JobState, now: number, until: number): { state: JobState; claimed: HeldAlert[]; stale: HeldAlert[] } {\n const next = cloneState(state);\n const queue = next.undelivered!;\n const stale = queue.filter((alert) => staleAlert(alert, next));\n const silenced = isSilenced(next, now);\n const room = Math.max(0, MAX_UNDELIVERED - (next.sending ?? []).length);\n const claimed = queue.filter((alert) => !stale.includes(alert) && (!silenced || alert.type === \"recovered\")).slice(0, room);\n if (stale.length === 0 && claimed.length === 0) return { state, claimed, stale };\n next.undelivered = queue.filter((alert) => !stale.includes(alert) && !claimed.includes(alert));\n if (claimed.length > 0) next.sending = [...(next.sending ?? []), ...claimed.map((alert) => ({ until, alert }))];\n return { state: next, claimed, stale };\n}\n\n/** Runs held in `pendingRuns` per job; past it the oldest go. */\nexport const MAX_PENDING_RUNS = 100;\n\n/**\n * A run just started, held in `pendingRuns` until its finish is evaluated\n * (releaseRun), so a check can evaluate it if the process that finished it\n * stops before writing the evaluation. Written with onRunStart's state.\n * Only the newest MAX_PENDING_RUNS are kept.\n */\nexport function holdRun(state: JobState, runId: string): JobState {\n const next = cloneState(state);\n const held = (next.pendingRuns ?? []).filter((id) => id !== runId);\n held.push(runId);\n next.pendingRuns = held.slice(-MAX_PENDING_RUNS);\n return next;\n}\n\n/** A run taken out of `pendingRuns`, as its evaluation is written or once it is gone from the store. */\nexport function releaseRun(state: JobState, runId: string): JobState {\n const held = state.pendingRuns ?? [];\n if (!held.includes(runId)) return state;\n const next = cloneState(state);\n const left = held.filter((id) => id !== runId);\n if (left.length > 0) next.pendingRuns = left;\n else delete next.pendingRuns;\n return next;\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/**\n * The metrics of a successful run that fell below their floor. A metric\n * with a floor breaches when it reports less than that floor. One without\n * breaches when it reports 0 or less and either it did so on the job's last\n * successful run too (`previous`, the metrics under their floor then), or\n * the earlier successful runs that reported it (at least five, the newest\n * twenty) all reported more than 0. So a job that keeps writing nothing stays\n * under its floor however long it goes on, and a metric that is always 0\n * never alerts.\n */\nexport function floorBreaches(def: Pick<JobDefinition, \"floor\">, run: Run, history: Run[], previous: readonly string[] = []): BudgetBreach[] {\n const breaches: BudgetBreach[] = [];\n for (const [metric, value] of Object.entries(run.metrics)) {\n const floor = def.floor?.[metric];\n if (floor !== undefined) {\n if (value < floor) breaches.push({ metric, value, limit: floor, basis: \"floor\" });\n continue;\n }\n if (value > 0) continue;\n if (previous.includes(metric)) {\n breaches.push({ metric, value, limit: 0, basis: \"0 or less on the run before too\" });\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 || !past.every((v) => v > 0)) continue;\n const lowest = Math.min(...past);\n breaches.push({ metric, value, limit: lowest, basis: `the last ${past.length} runs all reported more than 0, the lowest ${formatNumber(lowest)}` });\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/** When a run finished, or null while it runs (a run marked timeout has finished). */\nexport function finishedAtOf(run: Pick<Run, \"status\" | \"finishedAt\">): number | null {\n return run.status !== \"running\" && typeof run.finishedAt === \"number\" ? run.finishedAt : null;\n}\n\n/**\n * Whether `run` brings the schedule up to date at `now`, so missed may close\n * for it: judged with it as the job's last run, as onCheck judges, the run\n * the schedule wants next is not yet past its deadline. A run that started\n * before the fire missed opened for does not cover that fire, however late\n * it finishes, and neither does a run a source reports only once it is over\n * whose start was before it. Every run starting now covers it. A job with no\n * schedule, or one that no longer parses, has nothing due, so any run does.\n */\nexport function catchesUp(def: JobDefinition | StoredJobDefinition, run: Pick<Run, \"startedAt\" | \"status\" | \"finishedAt\">, now: number): boolean {\n if (!def.schedule) return true;\n try {\n const exp = expectation(parseSchedule(def.schedule, def.timezone), run.startedAt, run.startedAt, graceMs(def), finishedAtOf(run));\n return exp === null || now <= exp.deadline;\n } catch {\n return true;\n }\n}\n\n/**\n * Called when a run starts, or when a run a source reports is recorded.\n * Missed and stuck are about the absence of a run, so a run starting closes\n * them without an alert; the recovered message waits for a successful\n * finish. Missed closes only when the run catches the schedule up\n * (catchesUp), which a run starting now always does.\n */\nexport function onRunStart(def: JobDefinition | StoredJobDefinition, run: Pick<Run, \"startedAt\" | \"status\" | \"finishedAt\">, state: JobState, now: number): JobState {\n const next = cloneState(state);\n if (catchesUp(def, run, now)) 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. Missed\n * closes only when the run catches the schedule up (catchesUp): a run that\n * started before the fire missed opened for leaves it open.\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 if (catchesUp(def, run, now)) 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 short = floorBreaches(def, run, history, next.underFloor);\n if (short.length > 0) {\n next.underFloor = short.map((b) => b.metric);\n if (openCondition(next, \"under_floor\", now)) {\n alerts.push({ type: \"under_floor\", run, details: { breaches: short } });\n }\n } else {\n delete next.underFloor;\n closeCondition(next, \"under_floor\");\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 if (catchesUp(def, run, now)) 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, lastRun ? finishedAtOf(lastRun) : null);\n const nextExpectedAt = parsed.kind === \"interval\" ? exp!.dueAt : 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, or\n // after it finished when it outlasted the period (intervalDue). If that run\n // 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 // A completed import keeps its finish while its evaluation intent is written.\n return run.status === \"running\" && run.finishedAt === null && now - run.startedAt > timeoutMs(def);\n}\n\n/**\n * Whether a finished import was left as its placeholder: recordRun() writes\n * one as running, with `finishedAt` set to when it was written, and the\n * finish that follows never landed (the importer stopped, or a store write\n * failed and it was not retried). A check closes one SEND_LEASE_MS after\n * it was written, as it closes a stuck run.\n */\nexport function isStranded(run: Run, now: number): boolean {\n return run.status === \"running\" && run.finishedAt !== null && now - run.finishedAt > SEND_LEASE_MS;\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 no new alert is sent, but a recovery is. A\n * condition can only close during a silence if it opened (and alerted)\n * before it, so its recovery closes an incident someone was told about.\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: evaluation.alerts.filter((alert) => alert.type === \"recovered\") };\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. From a\n * foreign or damaged row: an alert whose `at` is not a number, and a\n * recovery whose `details.after` is not a list of strings, are stale.\n */\nexport function staleAlert(alert: AlertDraft & { at: number }, state: JobState): boolean {\n if (alert.type === \"recovered\") {\n // One whose details say nothing of what it recovers from (a foreign or damaged row's) cannot be judged, and goes.\n const after: unknown = isObject(alert.details) ? (alert.details as { after?: unknown }).after : undefined;\n if (!Array.isArray(after)) return true;\n return after.some((condition) => typeof condition !== \"string\" || state.open[condition as Condition] !== undefined);\n }\n // One with no time (a foreign or damaged row's) cannot match an open condition.\n return typeof alert.at !== \"number\" || 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, floor, 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 (floor !== undefined) out.floor = floor;\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)] }).startChecking();\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 /** Failed writes are retried by id, preserving whether they were historical backfill. */\n const retry = new Map<number, boolean>();\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 // One forgotten since it was declared (the dashboard's forget) is declared again, though\n // unchanged: recordRun takes runs only of a declared job.\n const live = host.definedJobs ? new Set(host.definedJobs().map((d) => d.name)) : null;\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) || (live !== null && !live.has(name))) {\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 as StoredJobDefinition | null;\n // A foreign or damaged definition (not an object, tags not a list) is not one of ours.\n if (typeof def !== \"object\" || def === null || !Array.isArray(def.tags)) continue;\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(typeof def.description === \"string\" ? 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 // The names declared now, after the retires above. A run copied under\n // a retired name that was then forgotten (the dashboard's forget) has\n // no job to go to: it is let go, never recorded, and never read again.\n const declaredNow = host.definedJobs ? new Set(host.definedJobs().map((d) => d.name)) : null;\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 evaluate = retry.get(runid) ?? evaluate;\n const jobid = Number(row.jobid);\n const name = pending.get(runid) ?? names.get(jobid);\n if (!name || (declaredNow !== null && !inUse.has(name) && !declaredNow.has(name))) {\n pending.delete(runid);\n held.delete(runid);\n retry.delete(runid);\n if (name) retired.delete(name);\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 retry.set(runid, evaluate);\n host.onError(e, `source pg_cron: run ${runid}`);\n return;\n }\n retry.delete(runid);\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(), ...retry.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 retry.delete(runid);\n }\n }\n return alerts;\n },\n };\n}\n","// @cronwatch/sdk/pg-cron: what the entry point exports. The helpers the\n// source is built from are internal; the names below are kept, deprecated,\n// for anyone who imported them, and leave the exports in 1.0.\nimport * as internal from \"../sources/pgcron.js\";\n\nexport { pgCron } from \"../sources/pgcron.js\";\nexport type { PgCronJob, PgCronOptions, Queryable } from \"../sources/pgcron.js\";\n\n/** @deprecated Internal to the pg_cron source; leaves the exports in 1.0. */\nexport const PG_CRON_HOLD_MS = internal.PG_CRON_HOLD_MS;\n/** @deprecated Internal to the pg_cron source; leaves the exports in 1.0. */\nexport const pgCronSchedule = internal.pgCronSchedule;\n/** @deprecated Internal to the pg_cron source; leaves the exports in 1.0. */\nexport const pgCronJobName = internal.pgCronJobName;\n/** @deprecated Internal to the pg_cron source; leaves the exports in 1.0. */\nexport const pgCronRun = internal.pgCronRun;\n"]}
1
+ {"version":3,"sources":["../src/evaluate.ts","../src/sources/pgcron.ts","../src/entries/pg-cron.ts"],"names":["PG_CRON_HOLD_MS","pgCronSchedule","pgCronJobName","pgCronRun"],"mappings":"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","file":"pg-cron.js","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 Alert,\n AlertDraft,\n BudgetBreach,\n Condition,\n HeldAlert,\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 * When a silence of `ms` from `now` ends: a whole millisecond, never past\n * MAX_DURATION_MS (2^53 - 1), however long the silence asked for. Every port\n * sharing the store reads it back unchanged, where a larger number could\n * wrap to a time long past and send alerts during the silence.\n */\nexport function silenceEnd(now: number, ms: number): number {\n return Math.min(now + Math.floor(Math.min(ms, MAX_DURATION_MS)), MAX_DURATION_MS);\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 JSON object: not null, not an array. */\nexport function isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * A stored state with every field present, or a fresh one. State written by\n * an older version lacks the newer fields. `sending` and `underFloor` are the\n * exceptions: each is there only while it holds something.\n *\n * Read leniently, since a foreign, hand-edited, or damaged row must affect\n * only its own job, and the next write puts it right: a state that is not\n * an object reads as none; `open` keeps only its entries whose value is a\n * number (anything but an object reads as {}); `silencedUntil` and\n * `lastAlertAt` that are not numbers read as null; `pendingRecovery` keeps\n * only its strings, and `undelivered` only its entries that are objects (a\n * list of neither shape reads as []), and `underFloor` and `pendingRuns`\n * only their strings (each absent when none). Unknown fields are kept as\n * written.\n */\nexport function normalizeState(state: JobState | null, job: string): JobState {\n if (!isObject(state)) return emptyState(job);\n const { sending, underFloor, pendingRuns, ...rest } = state;\n const open = isObject(state.open)\n ? Object.fromEntries(Object.entries(state.open).filter(([, at]) => typeof at === \"number\"))\n : {};\n const number = (value: unknown) => (typeof value === \"number\" ? value : null);\n const list = (value: unknown) => (Array.isArray(value) ? value : []);\n return {\n ...emptyState(job),\n ...rest,\n open,\n consecutiveFailures: failureCount(state),\n silencedUntil: number(state.silencedUntil),\n lastAlertAt: number(state.lastAlertAt),\n pendingRecovery: list(state.pendingRecovery).filter((c): c is Condition => typeof c === \"string\"),\n undelivered: list(state.undelivered).filter((alert): alert is Alert => isObject(alert)),\n ...(Array.isArray(sending) && sending.length > 0 ? { sending: [...sending] } : {}),\n ...(Array.isArray(underFloor) && underFloor.some((m) => typeof m === \"string\") ? { underFloor: underFloor.filter((m): m is string => typeof m === \"string\") } : {}),\n ...(Array.isArray(pendingRuns) && pendingRuns.some((id) => typeof id === \"string\") ? { pendingRuns: pendingRuns.filter((id): id is string => typeof id === \"string\") } : {}),\n };\n}\n\n// ---------------------------------------------------------------- delivery\n\n/** Alerts kept per job for retry, and per job being sent; past it the oldest go. */\nexport const MAX_UNDELIVERED = 20;\n\n/**\n * How long an alert in `sending` is left to the process sending it. Longer\n * than any send takes: at most three alerts go out together, each with 25\n * seconds of triage and 15 of channels.\n */\nexport const SEND_LEASE_MS = 5 * 60_000;\n\n/** Identifies an alert across retries, and in `sending`. */\nexport function alertKey(alert: Pick<Alert, \"type\" | \"at\" | \"run\">): string {\n return `${alert.type}|${alert.at}|${alert.run?.id ?? \"\"}`;\n}\n\n/** How much of a held alert's run error and output is kept: 2048 UTF-16 units of each. */\nexport const HELD_TEXT_CAP = 2048;\n\n/**\n * How far past HELD_TEXT_CAP a text may run and still be kept whole: longer\n * than any marker, so a text trimmed once (its kept part and its marker) is\n * never trimmed again, and the count in its marker stays true.\n */\nexport const HELD_TEXT_SLACK = 64;\n\n/**\n * A run as an alert held in a job's state keeps it (`sending` and\n * `undelivered`), so the state stays small however much a run logged. Its\n * error keeps its first HELD_TEXT_CAP units and its output its last, each\n * cut so as not to split a surrogate pair, with a line saying how many\n * units were left out: \"<head>\\n[N more characters trimmed]\" and\n * \"[N earlier characters trimmed]\\n<tail>\". A text of at most\n * HELD_TEXT_CAP + HELD_TEXT_SLACK units, or that is not a string, is kept\n * as it is, and so is every other field. The first send of an alert uses\n * the alert as composed; only a retry reads the held copy.\n */\nexport function heldRun(run: Run): Run {\n const error = typeof run.error === \"string\" && run.error.length > HELD_TEXT_CAP + HELD_TEXT_SLACK ? keepHead(run.error) : run.error;\n const output = typeof run.output === \"string\" && run.output.length > HELD_TEXT_CAP + HELD_TEXT_SLACK ? keepTail(run.output) : run.output;\n return error === run.error && output === run.output ? run : { ...run, error, output };\n}\n\n/** How much of a held alert's message is kept: its first 4096 UTF-16 units. */\nexport const HELD_MESSAGE_CAP = 4096;\n\nfunction keepHead(text: string, cap = HELD_TEXT_CAP): string {\n let end = cap;\n if (isHighSurrogate(text.charCodeAt(end - 1))) end -= 1;\n return `${text.slice(0, end)}\\n[${text.length - end} more characters trimmed]`;\n}\n\nfunction keepTail(text: string): string {\n let start = text.length - HELD_TEXT_CAP;\n if (isLowSurrogate(text.charCodeAt(start)) && isHighSurrogate(text.charCodeAt(start - 1))) start += 1;\n return `[${start} earlier characters trimmed]\\n${text.slice(start)}`;\n}\n\nfunction isHighSurrogate(code: number): boolean {\n return code >= 0xd800 && code <= 0xdbff;\n}\n\nfunction isLowSurrogate(code: number): boolean {\n return code >= 0xdc00 && code <= 0xdfff;\n}\n\n/**\n * An alert as it is held in state: its run trimmed (heldRun), and its\n * message, when longer than HELD_MESSAGE_CAP + HELD_TEXT_SLACK units, cut to\n * its first HELD_MESSAGE_CAP like a run's error (\"<head>\\n[N more characters\n * trimmed]\"). Everything else, the title included, as it is. The first send\n * uses the alert as composed.\n */\nexport function heldAlert<T extends HeldAlert>(alert: T): T {\n const run: unknown = alert.run;\n const trimmedRun = isObject(run) ? heldRun(run as unknown as Run) : run;\n const message: unknown = alert.message;\n const trimmedMessage = typeof message === \"string\" && message.length > HELD_MESSAGE_CAP + HELD_TEXT_SLACK ? keepHead(message, HELD_MESSAGE_CAP) : message;\n if (trimmedRun === run && trimmedMessage === message) return alert;\n return { ...alert, run: trimmedRun, message: trimmedMessage } as T;\n}\n\n/**\n * Each configured channel's key, in the order configured: its name, with\n * \"#2\", \"#3\", and so on for the second and later channels of the same name,\n * so two webhooks are \"webhook\" and \"webhook#2\". A key already given to an\n * earlier channel (a channel named \"webhook#2\" itself, say) is skipped for\n * the next number up, so every key is different. A held alert names the\n * channels that still owe it by these keys (HeldAlert.owed).\n */\nexport function channelKeys(names: readonly string[]): string[] {\n const seen = new Map<string, number>();\n const taken = new Set<string>();\n return names.map((name) => {\n let n = (seen.get(name) ?? 0) + 1;\n seen.set(name, n);\n let key = n === 1 ? name : `${name}#${n}`;\n while (taken.has(key)) key = `${name}#${++n}`;\n taken.add(key);\n return key;\n });\n}\n\n/**\n * The channels a held alert is still owed by, of those configured now\n * (`configured`, channelKeys order): those in its `owed` that are still\n * configured, or every configured one when it has no `owed` list (an alert\n * held by a release before per-channel delivery, or one never sent). An\n * entry of `owed` that is not a string is ignored. None left means the\n * alert is done.\n */\nexport function owedChannels(alert: HeldAlert, configured: readonly string[]): string[] {\n const owed: unknown = alert.owed;\n if (!Array.isArray(owed)) return [...configured];\n return configured.filter((key) => owed.includes(key));\n}\n\n/**\n * An alert after a send to the channels in `tried`, of which those in\n * `failed` refused it: null when none is left owing it, else the alert\n * with `owed` set to those that refused, in configured order.\n */\nexport function afterSend<T extends HeldAlert>(alert: T, tried: readonly string[], failed: readonly string[]): T | null {\n const owed = tried.filter((key) => failed.includes(key));\n return owed.length === 0 ? null : { ...alert, owed };\n}\n\n/** An alert without its `owed` list, as a channel is given it. */\nexport function withoutOwed(alert: HeldAlert): Alert {\n if (!(\"owed\" in alert)) return alert;\n const { owed: _owed, ...rest } = alert;\n return rest as Alert;\n}\n\n/** Keeps the newest `max` of `list`, and says how many went. */\nfunction newest<T>(list: T[], max: number): { kept: T[]; dropped: number } {\n return { kept: list.slice(-max), dropped: Math.max(0, list.length - max) };\n}\n\n/**\n * `alerts` added to the undelivered queue, each with its run trimmed\n * (heldAlert) and its `owed` list as given: one with the same key as a\n * queued alert replaces it where it stands, the rest go at the end, and only\n * the newest MAX_UNDELIVERED stay. `dropped` counts those let go.\n */\nexport function queueUndelivered(state: JobState, alerts: HeldAlert[]): { state: JobState; dropped: number } {\n const next = cloneState(state);\n const byKey = new Map(alerts.map((alert) => [alertKey(alert), heldAlert(alert)]));\n const queue = next.undelivered!.map((alert) => byKey.get(alertKey(alert)) ?? alert);\n const known = new Set(queue.map(alertKey));\n queue.push(...[...byKey.values()].filter((alert) => !known.has(alertKey(alert))));\n const { kept, dropped } = newest(queue, MAX_UNDELIVERED);\n next.undelivered = kept;\n return { state: next, dropped };\n}\n\n/**\n * The outbox. Alerts just composed are written with the state that opens\n * their condition, before any is sent, so a process that stops part way does\n * not lose them: into `sending`, each with its lease ending at `until`, when\n * this process sends them, or (deliver: \"check\") straight into the\n * undelivered queue for a check elsewhere. Either way the copy held has its\n * run trimmed (heldAlert) and no `owed` list, so every configured channel\n * owes it; the first send uses the alerts as composed. `dropped` counts\n * alerts let go past MAX_UNDELIVERED.\n */\nexport function holdAlerts(state: JobState, alerts: Alert[], until: number, deferred: boolean): { state: JobState; dropped: number } {\n if (alerts.length === 0) return { state, dropped: 0 };\n if (deferred) return queueUndelivered(state, alerts);\n const next = cloneState(state);\n const { kept, dropped } = newest([...(next.sending ?? []), ...alerts.map((alert) => ({ until, alert: heldAlert(alert) }))], MAX_UNDELIVERED);\n next.sending = kept;\n return { state: next, dropped };\n}\n\n/**\n * Alerts in `sending` whose lease ran out by `now`: the process sending them\n * stopped before it recorded how the send went. They go to the undelivered\n * queue, where the retry sends them (with triage, which is never stored with\n * them here) or drops them as stale, owed by the channels its `owed` names.\n * An entry that is not an object with an alert is dropped; one without a\n * numeric `until` counts as run out.\n */\nexport function releaseSending(state: JobState, now: number): { state: JobState; dropped: number } {\n const sending = state.sending ?? [];\n const lapsed = sending.filter((entry) => !(typeof entry?.until === \"number\" && entry.until > now));\n if (lapsed.length === 0) return { state, dropped: 0 };\n const held = sending.filter((entry) => !lapsed.includes(entry));\n return queueUndelivered(\n { ...state, sending: held },\n lapsed.filter((entry) => typeof entry?.alert === \"object\" && entry.alert !== null).map((entry) => entry.alert),\n );\n}\n\n/**\n * How a send went. Delivered alerts (no channel owes them any more) and\n * stale ones (`dropped`) leave the queue; failed ones (some channel still\n * owes them, named by their `owed`) replace their queued copy, so a triage\n * made on this attempt and the shrunken `owed` are kept, or join the queue.\n * Every one of them leaves `sending`. lastAlertAt moves to `now` when\n * `reached` (some channel accepted an alert on this attempt; by default,\n * when any was delivered). `dropped` in the result counts alerts let go\n * past MAX_UNDELIVERED.\n */\nexport function recordSent(\n state: JobState,\n delivered: HeldAlert[],\n failed: HeldAlert[],\n stale: HeldAlert[],\n now: number,\n reached: boolean = delivered.length > 0,\n): { state: JobState; dropped: number } {\n const next = cloneState(state);\n const done = new Set([...delivered, ...stale].map(alertKey));\n next.undelivered = next.undelivered!.filter((alert) => !done.has(alertKey(alert)));\n const sent = new Set([...delivered, ...failed, ...stale].map(alertKey));\n const held = (next.sending ?? []).filter((entry) => !(typeof entry?.alert === \"object\" && entry.alert !== null && sent.has(alertKey(entry.alert))));\n if (held.length > 0) next.sending = held;\n else delete next.sending;\n if (reached) next.lastAlertAt = now;\n return queueUndelivered(next, failed);\n}\n\n/**\n * A check's claim on the undelivered queue, written before it sends, so two\n * processes checking at once never retry the same alert: the queued alerts\n * that still describe the job move into `sending`, oldest first, each with\n * its lease ending at `until`, as many as `sending` has room for under\n * MAX_UNDELIVERED (the rest wait for the next check); stale ones\n * (staleAlert) leave the queue. While the job is silenced only recoveries\n * are claimed. The process whose write lands sends `claimed`.\n */\nexport function claimUndelivered(state: JobState, now: number, until: number): { state: JobState; claimed: HeldAlert[]; stale: HeldAlert[] } {\n const next = cloneState(state);\n const queue = next.undelivered!;\n const stale = queue.filter((alert) => staleAlert(alert, next));\n const silenced = isSilenced(next, now);\n const room = Math.max(0, MAX_UNDELIVERED - (next.sending ?? []).length);\n const claimed = queue.filter((alert) => !stale.includes(alert) && (!silenced || alert.type === \"recovered\")).slice(0, room);\n if (stale.length === 0 && claimed.length === 0) return { state, claimed, stale };\n next.undelivered = queue.filter((alert) => !stale.includes(alert) && !claimed.includes(alert));\n if (claimed.length > 0) next.sending = [...(next.sending ?? []), ...claimed.map((alert) => ({ until, alert }))];\n return { state: next, claimed, stale };\n}\n\n/** Runs held in `pendingRuns` per job; past it the oldest go. */\nexport const MAX_PENDING_RUNS = 100;\n\n/**\n * A run just started, held in `pendingRuns` until its finish is evaluated\n * (releaseRun), so a check can evaluate it if the process that finished it\n * stops before writing the evaluation. Written with onRunStart's state.\n * Only the newest MAX_PENDING_RUNS are kept.\n */\nexport function holdRun(state: JobState, runId: string): JobState {\n const next = cloneState(state);\n const held = (next.pendingRuns ?? []).filter((id) => id !== runId);\n held.push(runId);\n next.pendingRuns = held.slice(-MAX_PENDING_RUNS);\n return next;\n}\n\n/** A run taken out of `pendingRuns`, as its evaluation is written or once it is gone from the store. */\nexport function releaseRun(state: JobState, runId: string): JobState {\n const held = state.pendingRuns ?? [];\n if (!held.includes(runId)) return state;\n const next = cloneState(state);\n const left = held.filter((id) => id !== runId);\n if (left.length > 0) next.pendingRuns = left;\n else delete next.pendingRuns;\n return next;\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/**\n * The metrics of a successful run that fell below their floor. A metric\n * with a floor breaches when it reports less than that floor. One without\n * breaches when it reports 0 or less and either it did so on the job's last\n * successful run too (`previous`, the metrics under their floor then), or\n * the earlier successful runs that reported it (at least five, the newest\n * twenty) all reported more than 0. So a job that keeps writing nothing stays\n * under its floor however long it goes on, and a metric that is always 0\n * never alerts.\n */\nexport function floorBreaches(def: Pick<JobDefinition, \"floor\">, run: Run, history: Run[], previous: readonly string[] = []): BudgetBreach[] {\n const breaches: BudgetBreach[] = [];\n for (const [metric, value] of Object.entries(run.metrics)) {\n const floor = def.floor?.[metric];\n if (floor !== undefined) {\n if (value < floor) breaches.push({ metric, value, limit: floor, basis: \"floor\" });\n continue;\n }\n if (value > 0) continue;\n if (previous.includes(metric)) {\n breaches.push({ metric, value, limit: 0, basis: \"0 or less on the run before too\" });\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 || !past.every((v) => v > 0)) continue;\n const lowest = Math.min(...past);\n breaches.push({ metric, value, limit: lowest, basis: `the last ${past.length} runs all reported more than 0, the lowest ${formatNumber(lowest)}` });\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/** When a run finished, or null while it runs (a run marked timeout has finished). */\nexport function finishedAtOf(run: Pick<Run, \"status\" | \"finishedAt\">): number | null {\n return run.status !== \"running\" && typeof run.finishedAt === \"number\" ? run.finishedAt : null;\n}\n\n/**\n * Whether `run` brings the schedule up to date at `now`, so missed may close\n * for it: judged with it as the job's last run, as onCheck judges, the run\n * the schedule wants next is not yet past its deadline. A run that started\n * before the fire missed opened for does not cover that fire, however late\n * it finishes, and neither does a run a source reports only once it is over\n * whose start was before it. Every run starting now covers it. A job with no\n * schedule, or one that no longer parses, has nothing due, so any run does.\n */\nexport function catchesUp(def: JobDefinition | StoredJobDefinition, run: Pick<Run, \"startedAt\" | \"status\" | \"finishedAt\">, now: number): boolean {\n if (!def.schedule) return true;\n try {\n const exp = expectation(parseSchedule(def.schedule, def.timezone), run.startedAt, run.startedAt, graceMs(def), finishedAtOf(run));\n return exp === null || now <= exp.deadline;\n } catch {\n return true;\n }\n}\n\n/**\n * Called when a run starts, or when a run a source reports is recorded.\n * Missed is about the absence of a run, so a run starting closes it without\n * an alert; the recovered message waits for a successful finish. It closes\n * only when the run catches the schedule up (catchesUp), which a run\n * starting now always does. Nothing else closes here. Stuck in particular\n * stays open: a run that has only started may hang as the last one did, and\n * closing stuck for it would open it, and alert, again at every timeout of\n * a job that hangs every time. Only a run that finishes ok closes stuck.\n */\nexport function onRunStart(def: JobDefinition | StoredJobDefinition, run: Pick<Run, \"startedAt\" | \"status\" | \"finishedAt\">, state: JobState, now: number): JobState {\n const next = cloneState(state);\n if (catchesUp(def, run, now)) closeCondition(next, \"missed\");\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. Missed\n * closes only when the run catches the schedule up (catchesUp): a run that\n * started before the fire missed opened for leaves it open.\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 if (catchesUp(def, run, now)) 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 short = floorBreaches(def, run, history, next.underFloor);\n if (short.length > 0) {\n next.underFloor = short.map((b) => b.metric);\n if (openCondition(next, \"under_floor\", now)) {\n alerts.push({ type: \"under_floor\", run, details: { breaches: short } });\n }\n } else {\n delete next.underFloor;\n closeCondition(next, \"under_floor\");\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 if (catchesUp(def, run, now)) 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, lastRun ? finishedAtOf(lastRun) : null);\n const nextExpectedAt = parsed.kind === \"interval\" ? exp!.dueAt : 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, or\n // after it finished when it outlasted the period (intervalDue). If that run\n // 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 // A completed import keeps its finish while its evaluation intent is written.\n return run.status === \"running\" && run.finishedAt === null && now - run.startedAt > timeoutMs(def);\n}\n\n/**\n * Whether a finished import was left as its placeholder: recordRun() writes\n * one as running, with `finishedAt` set to when it was written, and the\n * finish that follows never landed (the importer stopped, or a store write\n * failed and it was not retried). A check closes one SEND_LEASE_MS after\n * it was written: as the failed run it was when the placeholder holds an\n * error (only a run that failed has one), else as it closes a stuck run.\n */\nexport function isStranded(run: Run, now: number): boolean {\n return run.status === \"running\" && run.finishedAt !== null && now - run.finishedAt > SEND_LEASE_MS;\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 no new alert is sent, but a recovery is. A\n * condition can only close during a silence if it opened (and alerted)\n * before it, so its recovery closes an incident someone was told about.\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: evaluation.alerts.filter((alert) => alert.type === \"recovered\") };\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. From a\n * foreign or damaged row: an alert whose `at` is not a number, and a\n * recovery whose `details.after` is not a list of strings, are stale.\n */\nexport function staleAlert(alert: AlertDraft & { at: number }, state: JobState): boolean {\n if (alert.type === \"recovered\") {\n // One whose details say nothing of what it recovers from (a foreign or damaged row's) cannot be judged, and goes.\n const after: unknown = isObject(alert.details) ? (alert.details as { after?: unknown }).after : undefined;\n if (!Array.isArray(after)) return true;\n return after.some((condition) => typeof condition !== \"string\" || state.open[condition as Condition] !== undefined);\n }\n // One with no time (a foreign or damaged row's) cannot match an open condition.\n return typeof alert.at !== \"number\" || 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, floor, 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 (floor !== undefined) out.floor = floor;\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)] }).startChecking();\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 /** Failed writes are retried by id, preserving whether they were historical backfill. */\n const retry = new Map<number, boolean>();\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 // One forgotten since it was declared (the dashboard's forget) is declared again, though\n // unchanged: recordRun takes runs only of a declared job.\n const live = host.definedJobs ? new Set(host.definedJobs().map((d) => d.name)) : null;\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) || (live !== null && !live.has(name))) {\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 as StoredJobDefinition | null;\n // A foreign or damaged definition (not an object, tags not a list) is not one of ours.\n if (typeof def !== \"object\" || def === null || !Array.isArray(def.tags)) continue;\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(typeof def.description === \"string\" ? 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 // The names declared now, after the retires above. A run copied under\n // a retired name that was then forgotten (the dashboard's forget) has\n // no job to go to: it is let go, never recorded, and never read again.\n const declaredNow = host.definedJobs ? new Set(host.definedJobs().map((d) => d.name)) : null;\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 evaluate = retry.get(runid) ?? evaluate;\n const jobid = Number(row.jobid);\n const name = pending.get(runid) ?? names.get(jobid);\n if (!name || (declaredNow !== null && !inUse.has(name) && !declaredNow.has(name))) {\n pending.delete(runid);\n held.delete(runid);\n retry.delete(runid);\n if (name) retired.delete(name);\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 retry.set(runid, evaluate);\n host.onError(e, `source pg_cron: run ${runid}`);\n return;\n }\n retry.delete(runid);\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(), ...retry.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 retry.delete(runid);\n }\n }\n return alerts;\n },\n };\n}\n","// @cronwatch/sdk/pg-cron: what the entry point exports. The helpers the\n// source is built from are internal; the names below are kept, deprecated,\n// for anyone who imported them, and leave the exports in 1.0.\nimport * as internal from \"../sources/pgcron.js\";\n\nexport { pgCron } from \"../sources/pgcron.js\";\nexport type { PgCronJob, PgCronOptions, Queryable } from \"../sources/pgcron.js\";\n\n/** @deprecated Internal to the pg_cron source; leaves the exports in 1.0. */\nexport const PG_CRON_HOLD_MS = internal.PG_CRON_HOLD_MS;\n/** @deprecated Internal to the pg_cron source; leaves the exports in 1.0. */\nexport const pgCronSchedule = internal.pgCronSchedule;\n/** @deprecated Internal to the pg_cron source; leaves the exports in 1.0. */\nexport const pgCronJobName = internal.pgCronJobName;\n/** @deprecated Internal to the pg_cron source; leaves the exports in 1.0. */\nexport const pgCronRun = internal.pgCronRun;\n"]}
package/dist/postgres.cjs CHANGED
@@ -176,11 +176,25 @@ function rowToState(r) {
176
176
  }
177
177
 
178
178
  // src/stores/postgres.ts
179
+ var CONNECT_TIMEOUT_MS = 5e3;
179
180
  function postgres(options = {}) {
180
181
  const p = tablePrefix(options.prefix);
181
182
  const sql = statements("postgres", p);
182
183
  const owned = !options.pool;
183
- const pool = options.pool ?? new pg__default.default.Pool({ connectionString: options.connectionString ?? process.env.DATABASE_URL });
184
+ const pool = options.pool ?? new pg__default.default.Pool({
185
+ connectionTimeoutMillis: CONNECT_TIMEOUT_MS,
186
+ keepAlive: true,
187
+ ...options.poolOptions,
188
+ connectionString: options.connectionString ?? options.poolOptions?.connectionString ?? process.env.DATABASE_URL
189
+ });
190
+ if (owned) {
191
+ pool.on("error", (error) => {
192
+ try {
193
+ options.onError?.(error);
194
+ } catch {
195
+ }
196
+ });
197
+ }
184
198
  const transaction = async (work) => {
185
199
  const client = await pool.connect();
186
200
  let broken;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/output.ts","../src/stores/sql.ts","../src/stores/postgres.ts"],"names":["text","json","v","pg"],"mappings":"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","file":"postgres.cjs","sourcesContent":["/** Output is capped so a chatty job cannot fill the store. The tail is kept. */\nexport const OUTPUT_CAP = 16 * 1024;\n\n/**\n * NUL characters are removed first, since Postgres refuses them in TEXT and\n * JSONB and the whole run row would be lost. The cap then applies to what is\n * left.\n */\nexport function capOutput(text: string): string {\n const clean = stripNul(text);\n if (clean.length <= OUTPUT_CAP) return clean;\n return TRIMMED + clean.slice(clean.length - OUTPUT_CAP);\n}\n\nconst TRIMMED = \"[earlier output trimmed]\\n\";\n\n/**\n * How much text before the kept tail redaction reads, and never keeps: three\n * times the longest secret a default pattern can match (a PEM key's 16 KB body\n * with its header and footer, under OUTPUT_CAP + 1024), since a replacement\n * grows what it replaces at most threefold.\n */\nexport const REDACT_EDGE = 3 * (OUTPUT_CAP + 1024);\n\n/**\n * Output or an error as it is stored: redacted, then capped like capOutput,\n * so the cut cannot fall inside a secret and keep what follows its label.\n * Text of at most OUTPUT_CAP + REDACT_EDGE is redacted whole. Longer text is\n * cut to that many units from its end first, and after redacting, the first\n * REDACT_EDGE units are never kept: a secret whose label fell before that cut\n * is left out with them. NULs go before and after `redact`.\n */\nexport function redactAndCap(text: string, redact: (text: string) => string): string {\n const clean = stripNul(text);\n const from = clean.length - (OUTPUT_CAP + REDACT_EDGE);\n if (from <= 0) return capOutput(redact(clean));\n const redacted = stripNul(redact(clean.slice(from)));\n return TRIMMED + redacted.slice(Math.max(redacted.length - OUTPUT_CAP, REDACT_EDGE));\n}\n\n/** Removes every U+0000. */\nexport function stripNul(text: string): string {\n return text.includes(\"\\u0000\") ? text.replace(/\\u0000/g, \"\") : text;\n}\n\n/**\n * Removes every U+0000 from JSON text, keys and strings alike, by dropping\n * each \\u0000 escape (a NUL can appear in JSON no other way). Escapes are\n * read left to right in pairs, so an escaped backslash followed by \"u0000\"\n * is left as it is.\n */\nexport function stripJsonNul(json: string): string {\n return json.includes(\"\\\\u0000\") ? json.replace(/\\\\(u0000|[\\s\\S])/g, (escape, next: string) => (next === \"u0000\" ? \"\" : escape)) : json;\n}\n\n/** \"Name: message\" and the first five stack frames, capped like output. */\nexport function errorMessage(error: unknown): string {\n return capOutput(describeError(error));\n}\n\n/** \"Name: message\" and the first five stack frames, not capped: see redactAndCap. */\nexport function describeError(error: unknown): string {\n if (error instanceof Error) {\n // The stack repeats the header (over several lines when the message has\n // newlines), so take only its frames.\n const frames = (error.stack ?? \"\").split(\"\\n\").filter((line) => /^\\s+at /.test(line)).slice(0, 5);\n return `${error.name}: ${error.message}${frames.length ? `\\n${frames.join(\"\\n\")}` : \"\"}`;\n }\n if (typeof error === \"string\") return error;\n try {\n return JSON.stringify(error) ?? String(error);\n } catch {\n return String(error);\n }\n}\n\nconst REDACTED = \"[redacted]\";\n\n// Bounded quantifiers throughout, so a long line cannot make these backtrack.\n// They apply in this order, each to the text the ones before it left.\nconst SECRET_PATTERNS: [RegExp, string | ((match: string, ...groups: string[]) => string)][] = [\n // A PEM private key, header to footer. Without a footer (the output was\n // trimmed) it runs to the end of the base64 body. A \"-\" that starts five\n // dashes ends the body, so the footer is never swallowed into it.\n [\n /-----BEGIN (?:[A-Z0-9]{1,20} ){0,3}PRIVATE KEY-----(?:[A-Za-z0-9+/=\\s,:]|-(?!----)){0,16384}(?:-----END (?:[A-Z0-9]{1,20} ){0,3}PRIVATE KEY-----)?/g,\n REDACTED,\n ],\n // password=..., API_KEY: ..., \"client_secret\": \"...\", TOKEN='...', token=...,\n // :password=>\"...\" (but not max_tokens: 800). A quoted value is blanked to\n // its closing quote, spaces and all, and keeps its quotes.\n [\n /\\b([A-Za-z0-9_-]{0,40}(?:secret|token|passw(?:or)?d|pwd|api[_-]?key|access[_-]?key|private[_-]?key|credential)[A-Za-z0-9_-]{0,40}(?<![Tt][Oo][Kk][Ee][Nn][Ss])\"?\\s{0,3}(?:=>|[=:])\\s{0,3})(?:(\")[^\"\\n]{1,4096}\"|(')[^'\\n]{1,4096}'|[\"']?[^\\s\"',;&]{1,4096})/gi,\n (_match, name: string, double?: string, single?: string) => {\n const quote = double ?? single ?? \"\";\n return `${name}${quote}${REDACTED}${quote}`;\n },\n ],\n // Authorization: Basic <base64> and Authorization: Token <token>, also as a JSON or hash entry.\n [/\\b((?:proxy-)?authorization[\"']?\\s{0,3}(?:=>|[=:])\\s{0,3}[\"']?\\s{0,3}(?:basic|token)\\s{1,3})[A-Za-z0-9._~+/=:-]{1,4096}/gi, `$1${REDACTED}`],\n // Credentials inside a URL: postgres://user:password@host. The password\n // runs to the last \"@\" before a \"/\" or a space, so one that contains \"@\"\n // is blanked whole.\n [/(\\b[a-z][a-z0-9+.-]{0,30}:\\/\\/[^\\s/:@]{0,256}:)[^\\s/]{1,256}@/gi, `$1${REDACTED}@`],\n // Authorization: Bearer <token>\n [/\\b(Bearer\\s{1,3})[A-Za-z0-9._~+/=-]{8,4096}/g, `$1${REDACTED}`],\n // A bare JWT: three base64url segments, the first starting eyJ.\n [/\\beyJ[A-Za-z0-9_-]{4,4096}\\.[A-Za-z0-9_-]{4,4096}\\.[A-Za-z0-9_-]{0,4096}/g, REDACTED],\n // Incoming webhook URLs carry their secret in the path.\n [/(\\bhooks\\.slack\\.com\\/(?:services|workflows|triggers)\\/)[A-Za-z0-9/_-]{1,255}/gi, `$1${REDACTED}`],\n [/(\\bdiscord(?:app)?\\.com\\/api\\/(?:v\\d{1,2}\\/)?webhooks\\/)[A-Za-z0-9/_-]{1,255}/gi, `$1${REDACTED}`],\n // Well-known token shapes: AWS, GitHub, Slack, Stripe, Anthropic, OpenAI, and Google style keys.\n [/\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b/g, REDACTED],\n [/\\b(?:gh[pousr]_[A-Za-z0-9]{30,255}|github_pat_[A-Za-z0-9_]{20,255})\\b/g, REDACTED],\n [/\\bxox[abposr]-[A-Za-z0-9-]{10,255}/g, REDACTED],\n [/\\b[rsp]k_(?:live|test)_[A-Za-z0-9]{10,255}\\b/g, REDACTED],\n [/\\bwhsec_[A-Za-z0-9+/=]{16,255}/g, REDACTED],\n [/\\bsk-[A-Za-z0-9_-]{20,255}/g, REDACTED],\n [/\\bAIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])/g, REDACTED],\n];\n\n/**\n * The default `redact`: blanks values that look like secrets (key=value pairs\n * with secret-ish names, Authorization headers, URL credentials, bearer\n * tokens, JWTs, PEM private keys, webhook URLs, and well-known token formats)\n * before output or an error is stored, shown, or sent anywhere.\n */\nexport function redactSecrets(text: string): string {\n let out = text;\n for (const [pattern, replacement] of SECRET_PATTERNS) out = out.replace(pattern, replacement as string);\n return out;\n}\n","import { stripJsonNul, stripNul } from \"../output.js\";\nimport 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, as stateVersion() reads it: a whole\n // number from 0 to 2^53 - 1, else 0 (none, or a foreign row's 1.5 or \"x\",\n // which must neither fail the statement nor refuse every write for good;\n // on SQLite, also text that is not JSON).\n // Each CASE tests the JSON type before any cast.\n const version = (column: string) => {\n if (pg) {\n const v = `(${column}->>'version')::numeric`;\n return `CASE WHEN jsonb_typeof(${column}->'version') <> 'number' THEN 0 WHEN ${v} % 1 = 0 AND ${v} BETWEEN 0 AND 9007199254740991 THEN ${v}::bigint ELSE 0 END`;\n }\n const v = `json_extract(${column}, '$.version')`;\n // Text that is not JSON at all (SQLite holds any) counts as 0 too, before json_type could fail on it.\n return `CASE WHEN NOT json_valid(${column}) THEN 0 WHEN json_type(${column}, '$.version') NOT IN ('integer', 'real') THEN 0 WHEN ${v} = CAST(${v} AS INTEGER) AND ${v} BETWEEN 0 AND 9007199254740991 THEN CAST(${v} AS INTEGER) ELSE 0 END`;\n };\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/**\n * Postgres refuses U+0000 in TEXT and JSONB, and a refused write loses the\n * whole row, so every dialect writes text without it: a run's trigger,\n * output, error, and metric names, and every key and string of a definition\n * and a state. Identifiers (a job's name, a run's id) are written as given;\n * the client refuses one with a NUL before it gets here.\n */\nconst text = (value: string | null) => (value === null ? null : stripNul(value));\nconst jsonText = (value: unknown) => stripJsonNul(JSON.stringify(value));\n\n// Parameters in statement order, so both drivers bind the same values.\nexport const params = {\n upsertJob: (definition: StoredJobDefinition, now: number) => [definition.name, jsonText(definition), now, now],\n insertRun: (run: Run) => [\n run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), stripNul(run.trigger),\n ],\n updateRun: (run: Run) => [run.status, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), run.id],\n updateRunIf: (run: Run, fromStatuses: Run[\"status\"][]) => [\n run.status, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), run.id, ...fromStatuses,\n ],\n setState: (state: JobState) => [state.job, jsonText(state)],\n casInsert: (state: JobState) => [state.job, jsonText(state)],\n casUpdate: (state: JobState, expectedVersion: number) => [jsonText(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\n/**\n * Rows are read leniently: a foreign, hand-edited, or damaged row (SQLite\n * keeps whatever type it is given, in any column) must affect only its own\n * job, never every read. JSON text that does not parse reads as null, which\n * the client takes as no state, or as an unreadable definition it reports.\n */\nconst json = (v: unknown): unknown => {\n if (typeof v !== \"string\") return v;\n try {\n return JSON.parse(v) as unknown;\n } catch {\n return null;\n }\n};\n/** A time, or a count of milliseconds, as a column holds it (a string from Postgres's BIGINT): NaN when it is not a number. */\nconst number = (v: unknown): number => (typeof v === \"number\" ? v : typeof v === \"string\" && v.trim() !== \"\" ? Number(v) : Number.NaN);\n/** A time that must be there: one that is not a finite number reads as 0. */\nconst time = (v: unknown): number => {\n const n = number(v);\n return Number.isFinite(n) ? n : 0;\n};\n/** A time or duration that may be absent: one that is not a finite number reads as null. */\nconst maybeTime = (v: unknown): number | null => {\n const n = number(v);\n return Number.isFinite(n) ? n : null;\n};\nconst textOrNull = (v: unknown): string | null => (typeof v === \"string\" ? v : null);\nconst isObject = (v: unknown): v is Record<string, number> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n\n/** A job's row. Its definition is as stored, or null when its text does not parse; the client reads the rest (see readStoredJob). */\nexport function rowToJob(r: JobRow): StoredJob {\n return { name: r.name, definition: json(r.definition) as StoredJobDefinition, createdAt: time(r.created_at), updatedAt: time(r.updated_at) };\n}\n\n/**\n * A run's row. A start that is not a finite number reads as 0, a finish or\n * duration as null; an error or output that is not text as null; metrics\n * that do not parse to an object as {}; a trigger that is not text as \"run\".\n */\nexport function rowToRun(r: RunRow): Run {\n const metrics = json(r.metrics);\n return {\n id: r.id, job: r.job, status: r.status, startedAt: time(r.started_at), finishedAt: maybeTime(r.finished_at),\n durationMs: maybeTime(r.duration_ms), error: textOrNull(r.error), output: textOrNull(r.output),\n metrics: isObject(metrics) ? metrics : {}, trigger: typeof r.trigger === \"string\" ? r.trigger : \"run\",\n };\n}\n\n/** A state's row, or null when its text does not parse (normalizeState reads anything else that is not an object as no state). */\nexport function rowToState(r: StateRow): JobState {\n return json(r.state) as JobState;\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 & Required<Pick<Store, \"lastRun\">> {\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 // A connection whose ROLLBACK fails (it died part way) is destroyed, not\n // handed back to the pool, and the error that stopped the work is the one thrown.\n const transaction = async (work: (client: pg.PoolClient) => Promise<void>) => {\n const client = await pool.connect();\n let broken: Error | undefined;\n try {\n await client.query(\"BEGIN\");\n await work(client);\n await client.query(\"COMMIT\");\n } catch (e) {\n try {\n await client.query(\"ROLLBACK\");\n } catch (rollback) {\n broken = rollback instanceof Error ? rollback : new Error(String(rollback));\n }\n throw e;\n } finally {\n client.release(broken);\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
+ {"version":3,"sources":["../src/output.ts","../src/stores/sql.ts","../src/stores/postgres.ts"],"names":["text","json","v","pg"],"mappings":";;;;;;;;;AAyCO,SAAS,SAASA,KAAAA,EAAsB;AAC7C,EAAA,OAAOA,KAAAA,CAAK,SAAS,IAAQ,CAAA,GAAIA,MAAK,OAAA,CAAQ,SAAA,EAAW,EAAE,CAAA,GAAIA,KAAAA;AACjE;AAQO,SAAS,aAAaC,KAAAA,EAAsB;AACjD,EAAA,OAAOA,KAAAA,CAAK,QAAA,CAAS,SAAS,CAAA,GAAIA,MAAK,OAAA,CAAQ,mBAAA,EAAqB,CAAC,MAAA,EAAQ,IAAA,KAAkB,IAAA,KAAS,OAAA,GAAU,EAAA,GAAK,MAAO,CAAA,GAAIA,KAAAA;AACpI;;;AC1CO,IAAM,cAAA,GAAiB,YAAA;AAG9B,IAAM,UAAA,GAAa,KAAK,kBAAA,CAAmB,MAAA;AAOpC,SAAS,WAAA,CAAY,SAAiB,cAAA,EAAwB;AACnE,EAAA,IAAI,CAAC,oBAAA,CAAqB,IAAA,CAAK,MAAM,CAAA,IAAK,MAAA,CAAO,SAAS,UAAA,EAAY;AACpE,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,mCAAmC,IAAA,CAAK,SAAA,CAAU,MAAM,CAAC,wFACjB,UAAU,CAAA,YAAA;AAAA,KACpD;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;AAEO,SAAS,MAAA,CAAO,SAAkB,CAAA,EAAmB;AAE1D,EAAA,MAAM,GAAA,GAAW,QAAA,CAAW;AAC5B,EAAA,MAAMA,KAAAA,GAAY,OAAA,CAAU;AAC5B,EAAA,OAAO;AAAA,+BAAA,EACwB,CAAC,CAAA;AAAA;AAAA,iBAAA,EAEfA,KAAI,CAAA;AAAA,iBAAA,EACJ,GAAG,CAAA;AAAA,iBAAA,EACH,GAAG,CAAA;AAAA;AAAA,+BAAA,EAEW,CAAC,CAAA,MAAA,EAAc,wBAAA,CAA6B;AAAA;AAAA;AAAA;AAAA,iBAAA,EAI1D,GAAG,CAAA;AAAA,kBAAA,EACF,GAAG,CAAA;AAAA,kBAAA,EACH,GAAG,CAAA;AAAA;AAAA;AAAA,cAAA,EAGPA,KAAI,CAAA;AAAA;AAAA;AAAA,+BAAA,EAGa,CAAC,uBAAuB,CAAC,CAAA;AAAA,+BAAA,EACzB,CAAC,mBAAmB,CAAC,CAAA;AAAA,+BAAA,EACrB,CAAC,CAAA;AAAA;AAAA,YAAA,EAEpBA,KAAI,CAAA;AAAA;AAAA,EAAA,CAAA;AAGlB;AAEO,SAAS,UAAA,CAAW,SAAkB,CAAA,EAAW;AAGtD,EAAA,MAAM,GAAA,GAAW,KAAA,CAAQ;AAEzB,EAAA,MAAM,MAAA,GAAc,CAAA,gBAAA,CAAA,CAAqB;AAMzC,EAAA,MAAM,OAAA,GAAU,CAAC,MAAA,KAAmB;AAClC,IAAQ;AACN,MAAA,MAAMC,EAAAA,GAAI,IAAI,MAAM,CAAA,sBAAA,CAAA;AACpB,MAAA,OAAO,0BAA0B,MAAM,CAAA,qCAAA,EAAwCA,EAAC,CAAA,aAAA,EAAgBA,EAAC,wCAAwCA,EAAC,CAAA,mBAAA,CAAA;AAAA,IAC5I;AAGmN,EACrN,CAAA;AACA,EAAA,MAAM,GAAA,GAAM;AAAA,IACV,SAAA,EAAW,eAAe,CAAC,CAAA;AAAA,yGAAA,CAAA;AAAA,IAE3B,MAAA,EAAQ,iBAAiB,CAAC,CAAA,mBAAA,CAAA;AAAA,IAC1B,QAAA,EAAU,CAAA,cAAA,EAAiB,CAAC,CAAA,cAAA,EAAiB,MAAM,CAAA,CAAA;AAAA,IACnD,UAAA,EAAY,eAAe,CAAC,CAAA,kBAAA,CAAA;AAAA,IAC5B,WAAA,EAAa,eAAe,CAAC,CAAA,mBAAA,CAAA;AAAA,IAC7B,SAAA,EAAW,eAAe,CAAC,CAAA,mBAAA,CAAA;AAAA,IAC3B,SAAA,EAAW,eAAe,CAAC,CAAA;AAAA,2CAAA,CAAA;AAAA,IAE3B,SAAA,EAAW,UAAU,CAAC,CAAA,sGAAA,CAAA;AAAA,IACtB,MAAA,EAAQ,iBAAiB,CAAC,CAAA,iBAAA,CAAA;AAAA,IAC1B,QAAA,EAAU,CAAA,cAAA,EAAiB,CAAC,CAAA,6CAAA,EAAgD,GAAG,CAAA,aAAA,CAAA;AAAA,IAC/E,WAAA,EAAa,CAAA,cAAA,EAAiB,CAAC,CAAA,mDAAA,EAAsD,GAAG,CAAA,CAAA;AAAA,IACxF,QAAA,EAAU,qBAAqB,CAAC,CAAA,mBAAA,CAAA;AAAA,IAChC,QAAA,EAAU,eAAe,CAAC,CAAA,uFAAA,CAAA;AAAA;AAAA;AAAA,IAG1B,SAAA,EAAW,eAAe,CAAC,CAAA;AAAA,mEAAA,EACsC,OAAA,CAAQ,CAAA,EAAG,CAAC,CAAA,WAAA,CAAa,CAAC,CAAA,IAAA,CAAA;AAAA,IAC3F,WAAW,CAAA,OAAA,EAAU,CAAC,CAAA,sCAAA,EAAyC,OAAA,CAAQ,OAAO,CAAC,CAAA,IAAA,CAAA;AAAA;AAAA;AAAA,IAG/E,KAAA,EAAO,eAAe,CAAC,CAAA;AAAA,sDAAA,EAC6B,CAAC,wBAAwB,CAAC,CAAA,SAAA;AAAA,GAChF;AACA,EAAQ;AACN,IAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,GAAG,CAAA,EAA2B;AAC1D,MAAA,IAAI,CAAA,GAAI,CAAA;AACR,MAAA,GAAA,CAAI,GAAG,CAAA,GAAI,GAAA,CAAI,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,MAAM,CAAA,CAAA,EAAI,EAAE,CAAC,CAAA,CAAE,CAAA;AAAA,IACpD;AAAA,EACF;AACA,EAAA,OAAO,GAAA;AACT;AAMO,SAAS,cAAA,CAAe,OAAA,EAAkB,CAAA,EAAW,KAAA,EAAuB;AACjF,EAAA,MAAMF,KAAAA,GAAO,CAAA,OAAA,EAAU,CAAC,CAAA,sHAAA,EAAyH,MAAM,IAAA,CAAK,EAAE,MAAA,EAAQ,KAAA,IAAS,MAAM,GAAG,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA,CAAA;AAEpM,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,OAAOA,MAAK,OAAA,CAAQ,KAAA,EAAO,MAAM,CAAA,CAAA,EAAI,EAAE,CAAC,CAAA,CAAE,CAAA;AAC5C;AASA,IAAM,OAAO,CAAC,KAAA,KAA0B,UAAU,IAAA,GAAO,IAAA,GAAO,SAAS,KAAK,CAAA;AAC9E,IAAM,WAAW,CAAC,KAAA,KAAmB,aAAa,IAAA,CAAK,SAAA,CAAU,KAAK,CAAC,CAAA;AAGhE,IAAM,MAAA,GAAS;AAAA,EACpB,SAAA,EAAW,CAAC,UAAA,EAAiC,GAAA,KAAgB,CAAC,UAAA,CAAW,IAAA,EAAM,QAAA,CAAS,UAAU,CAAA,EAAG,GAAA,EAAK,GAAG,CAAA;AAAA,EAC7G,SAAA,EAAW,CAAC,GAAA,KAAa;AAAA,IACvB,GAAA,CAAI,EAAA;AAAA,IAAI,GAAA,CAAI,GAAA;AAAA,IAAK,GAAA,CAAI,MAAA;AAAA,IAAQ,GAAA,CAAI,SAAA;AAAA,IAAW,GAAA,CAAI,UAAA;AAAA,IAAY,GAAA,CAAI,UAAA;AAAA,IAAY,IAAA,CAAK,IAAI,KAAK,CAAA;AAAA,IAAG,IAAA,CAAK,IAAI,MAAM,CAAA;AAAA,IAAG,QAAA,CAAS,IAAI,OAAO,CAAA;AAAA,IAAG,QAAA,CAAS,IAAI,OAAO;AAAA,GAC5J;AAAA,EACA,SAAA,EAAW,CAAC,GAAA,KAAa,CAAC,IAAI,MAAA,EAAQ,GAAA,CAAI,UAAA,EAAY,GAAA,CAAI,UAAA,EAAY,IAAA,CAAK,IAAI,KAAK,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA,EAAG,SAAS,GAAA,CAAI,OAAO,CAAA,EAAG,GAAA,CAAI,EAAE,CAAA;AAAA,EACtI,WAAA,EAAa,CAAC,GAAA,EAAU,YAAA,KAAkC;AAAA,IACxD,GAAA,CAAI,MAAA;AAAA,IAAQ,GAAA,CAAI,UAAA;AAAA,IAAY,GAAA,CAAI,UAAA;AAAA,IAAY,IAAA,CAAK,IAAI,KAAK,CAAA;AAAA,IAAG,IAAA,CAAK,IAAI,MAAM,CAAA;AAAA,IAAG,QAAA,CAAS,IAAI,OAAO,CAAA;AAAA,IAAG,GAAA,CAAI,EAAA;AAAA,IAAI,GAAG;AAAA,GACnH;AAAA,EACA,QAAA,EAAU,CAAC,KAAA,KAAoB,CAAC,MAAM,GAAA,EAAK,QAAA,CAAS,KAAK,CAAC,CAAA;AAAA,EAC1D,SAAA,EAAW,CAAC,KAAA,KAAoB,CAAC,MAAM,GAAA,EAAK,QAAA,CAAS,KAAK,CAAC,CAAA;AAAA,EAC3D,SAAA,EAAW,CAAC,KAAA,EAAiB,eAAA,KAA4B,CAAC,SAAS,KAAK,CAAA,EAAG,KAAA,CAAM,GAAA,EAAK,eAAe;AACvG,CAAA;AAmBA,IAAM,IAAA,GAAO,CAAC,CAAA,KAAwB;AACpC,EAAA,IAAI,OAAO,CAAA,KAAM,QAAA,EAAU,OAAO,CAAA;AAClC,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,CAAK,MAAM,CAAC,CAAA;AAAA,EACrB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF,CAAA;AAEA,IAAM,SAAS,CAAC,CAAA,KAAwB,OAAO,CAAA,KAAM,WAAW,CAAA,GAAI,OAAO,CAAA,KAAM,QAAA,IAAY,EAAE,IAAA,EAAK,KAAM,KAAK,MAAA,CAAO,CAAC,IAAI,MAAA,CAAO,GAAA;AAElI,IAAM,IAAA,GAAO,CAAC,CAAA,KAAuB;AACnC,EAAA,MAAM,CAAA,GAAI,OAAO,CAAC,CAAA;AAClB,EAAA,OAAO,MAAA,CAAO,QAAA,CAAS,CAAC,CAAA,GAAI,CAAA,GAAI,CAAA;AAClC,CAAA;AAEA,IAAM,SAAA,GAAY,CAAC,CAAA,KAA8B;AAC/C,EAAA,MAAM,CAAA,GAAI,OAAO,CAAC,CAAA;AAClB,EAAA,OAAO,MAAA,CAAO,QAAA,CAAS,CAAC,CAAA,GAAI,CAAA,GAAI,IAAA;AAClC,CAAA;AACA,IAAM,aAAa,CAAC,CAAA,KAA+B,OAAO,CAAA,KAAM,WAAW,CAAA,GAAI,IAAA;AAC/E,IAAM,QAAA,GAAW,CAAC,CAAA,KAA4C,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,KAAM,IAAA,IAAQ,CAAC,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA;AAG9G,SAAS,SAAS,CAAA,EAAsB;AAC7C,EAAA,OAAO,EAAE,IAAA,EAAM,CAAA,CAAE,MAAM,UAAA,EAAY,IAAA,CAAK,EAAE,UAAU,CAAA,EAA0B,SAAA,EAAW,IAAA,CAAK,EAAE,UAAU,CAAA,EAAG,WAAW,IAAA,CAAK,CAAA,CAAE,UAAU,CAAA,EAAE;AAC7I;AAOO,SAAS,SAAS,CAAA,EAAgB;AACvC,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,CAAA,CAAE,OAAO,CAAA;AAC9B,EAAA,OAAO;AAAA,IACL,IAAI,CAAA,CAAE,EAAA;AAAA,IAAI,KAAK,CAAA,CAAE,GAAA;AAAA,IAAK,QAAQ,CAAA,CAAE,MAAA;AAAA,IAAQ,SAAA,EAAW,IAAA,CAAK,CAAA,CAAE,UAAU,CAAA;AAAA,IAAG,UAAA,EAAY,SAAA,CAAU,CAAA,CAAE,WAAW,CAAA;AAAA,IAC1G,UAAA,EAAY,SAAA,CAAU,CAAA,CAAE,WAAW,CAAA;AAAA,IAAG,KAAA,EAAO,UAAA,CAAW,CAAA,CAAE,KAAK,CAAA;AAAA,IAAG,MAAA,EAAQ,UAAA,CAAW,CAAA,CAAE,MAAM,CAAA;AAAA,IAC7F,OAAA,EAAS,QAAA,CAAS,OAAO,CAAA,GAAI,UAAU,EAAC;AAAA,IAAG,SAAS,OAAO,CAAA,CAAE,OAAA,KAAY,QAAA,GAAW,EAAE,OAAA,GAAU;AAAA,GAClG;AACF;AAGO,SAAS,WAAW,CAAA,EAAuB;AAChD,EAAA,OAAO,IAAA,CAAK,EAAE,KAAK,CAAA;AACrB;;;AC9KA,IAAM,kBAAA,GAAqB,GAAA;AAOpB,SAAS,QAAA,CAAS,OAAA,GAA2B,EAAC,EAA6C;AAChG,EAAA,MAAM,CAAA,GAAI,WAAA,CAAY,OAAA,CAAQ,MAAM,CAAA;AACpC,EAAA,MAAM,GAAA,GAAM,UAAA,CAAW,UAAA,EAAY,CAAC,CAAA;AACpC,EAAA,MAAM,KAAA,GAAQ,CAAC,OAAA,CAAQ,IAAA;AACvB,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,IAAA,IAAQ,IAAIG,oBAAG,IAAA,CAAK;AAAA,IACvC,uBAAA,EAAyB,kBAAA;AAAA,IACzB,SAAA,EAAW,IAAA;AAAA,IACX,GAAG,OAAA,CAAQ,WAAA;AAAA,IACX,kBAAkB,OAAA,CAAQ,gBAAA,IAAoB,QAAQ,WAAA,EAAa,gBAAA,IAAoB,QAAQ,GAAA,CAAI;AAAA,GACpG,CAAA;AACD,EAAA,IAAI,KAAA,EAAO;AAIT,IAAA,IAAA,CAAK,EAAA,CAAG,OAAA,EAAS,CAAC,KAAA,KAAU;AAC1B,MAAA,IAAI;AACF,QAAA,OAAA,CAAQ,UAAU,KAAK,CAAA;AAAA,MACzB,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF,CAAC,CAAA;AAAA,EACH;AAIA,EAAA,MAAM,WAAA,GAAc,OAAO,IAAA,KAAmD;AAC5E,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,OAAA,EAAQ;AAClC,IAAA,IAAI,MAAA;AACJ,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,CAAO,MAAM,OAAO,CAAA;AAC1B,MAAA,MAAM,KAAK,MAAM,CAAA;AACjB,MAAA,MAAM,MAAA,CAAO,MAAM,QAAQ,CAAA;AAAA,IAC7B,SAAS,CAAA,EAAG;AACV,MAAA,IAAI;AACF,QAAA,MAAM,MAAA,CAAO,MAAM,UAAU,CAAA;AAAA,MAC/B,SAAS,QAAA,EAAU;AACjB,QAAA,MAAA,GAAS,oBAAoB,KAAA,GAAQ,QAAA,GAAW,IAAI,KAAA,CAAM,MAAA,CAAO,QAAQ,CAAC,CAAA;AAAA,MAC5E;AACA,MAAA,MAAM,CAAA;AAAA,IACR,CAAA,SAAE;AACA,MAAA,MAAA,CAAO,QAAQ,MAAM,CAAA;AAAA,IACvB;AAAA,EACF,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,MAAM,IAAA,GAAO;AAGX,MAAA,MAAM,WAAA,CAAY,OAAO,MAAA,KAAW;AAClC,QAAA,MAAM,OAAO,KAAA,CAAM,4CAAA,EAA8C,CAAC,CAAA,UAAA,EAAa,CAAC,EAAE,CAAC,CAAA;AACnF,QAAA,MAAM,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,UAAA,EAAY,CAAC,CAAC,CAAA;AAAA,MAC1C,CAAC,CAAA;AAAA,IACH,CAAA;AAAA,IACA,MAAM,SAAA,CAAU,UAAA,EAAY,GAAA,EAAK;AAC/B,MAAA,MAAM,IAAA,CAAK,MAAM,GAAA,CAAI,SAAA,EAAW,OAAO,SAAA,CAAU,UAAA,EAAY,GAAG,CAAC,CAAA;AAAA,IACnE,CAAA;AAAA,IACA,MAAM,OAAO,IAAA,EAAM;AACjB,MAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,IAAA,CAAK,MAAc,GAAA,CAAI,MAAA,EAAQ,CAAC,IAAI,CAAC,CAAA;AAC5D,MAAA,OAAO,KAAK,CAAC,CAAA,GAAI,SAAS,IAAA,CAAK,CAAC,CAAC,CAAA,GAAI,IAAA;AAAA,IACvC,CAAA;AAAA,IACA,MAAM,QAAA,GAAiC;AACrC,MAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,IAAA,CAAK,KAAA,CAAc,IAAI,QAAQ,CAAA;AACtD,MAAA,OAAO,IAAA,CAAK,IAAI,QAAQ,CAAA;AAAA,IAC1B,CAAA;AAAA,IACA,MAAM,UAAU,IAAA,EAAM;AACpB,MAAA,MAAM,WAAA,CAAY,OAAO,MAAA,KAAW;AAClC,QAAA,MAAM,OAAO,KAAA,CAAM,GAAA,CAAI,UAAA,EAAY,CAAC,IAAI,CAAC,CAAA;AACzC,QAAA,MAAM,OAAO,KAAA,CAAM,GAAA,CAAI,WAAA,EAAa,CAAC,IAAI,CAAC,CAAA;AAC1C,QAAA,MAAM,OAAO,KAAA,CAAM,GAAA,CAAI,SAAA,EAAW,CAAC,IAAI,CAAC,CAAA;AAAA,MAC1C,CAAC,CAAA;AAAA,IACH,CAAA;AAAA,IACA,MAAM,UAAU,GAAA,EAAK;AACnB,MAAA,MAAM,KAAK,KAAA,CAAM,GAAA,CAAI,WAAW,MAAA,CAAO,SAAA,CAAU,GAAG,CAAC,CAAA;AAAA,IACvD,CAAA;AAAA,IACA,MAAM,UAAU,GAAA,EAAK;AACnB,MAAA,MAAM,KAAK,KAAA,CAAM,GAAA,CAAI,WAAW,MAAA,CAAO,SAAA,CAAU,GAAG,CAAC,CAAA;AAAA,IACvD,CAAA;AAAA,IACA,MAAM,WAAA,CAAY,GAAA,EAAK,YAAA,EAAc;AACnC,MAAA,IAAI,YAAA,CAAa,MAAA,KAAW,CAAA,EAAG,OAAO,KAAA;AACtC,MAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,KAAA,CAAM,eAAe,UAAA,EAAY,CAAA,EAAG,YAAA,CAAa,MAAM,CAAA,EAAG,MAAA,CAAO,WAAA,CAAY,GAAA,EAAK,YAAY,CAAC,CAAA;AACzH,MAAA,OAAA,CAAQ,MAAA,CAAO,YAAY,CAAA,IAAK,CAAA;AAAA,IAClC,CAAA;AAAA,IACA,MAAM,OAAO,EAAA,EAAI;AACf,MAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,IAAA,CAAK,MAAc,GAAA,CAAI,MAAA,EAAQ,CAAC,EAAE,CAAC,CAAA;AAC1D,MAAA,OAAO,KAAK,CAAC,CAAA,GAAI,SAAS,IAAA,CAAK,CAAC,CAAC,CAAA,GAAI,IAAA;AAAA,IACvC,CAAA;AAAA,IACA,MAAM,QAAA,CAAS,GAAA,EAAK,KAAA,EAAO;AACzB,MAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,IAAA,CAAK,KAAA,CAAc,GAAA,CAAI,QAAA,EAAU,CAAC,GAAA,EAAK,KAAK,CAAC,CAAA;AACpE,MAAA,OAAO,IAAA,CAAK,IAAI,QAAQ,CAAA;AAAA,IAC1B,CAAA;AAAA,IACA,MAAM,QAAQ,GAAA,EAAK;AACjB,MAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,IAAA,CAAK,KAAA,CAAc,GAAA,CAAI,QAAA,EAAU,CAAC,GAAA,EAAK,CAAC,CAAC,CAAA;AAChE,MAAA,OAAO,KAAK,CAAC,CAAA,GAAI,SAAS,IAAA,CAAK,CAAC,CAAC,CAAA,GAAI,IAAA;AAAA,IACvC,CAAA;AAAA,IACA,MAAM,WAAA,GAAc;AAClB,MAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,IAAA,CAAK,KAAA,CAAc,IAAI,WAAW,CAAA;AACzD,MAAA,OAAO,IAAA,CAAK,IAAI,QAAQ,CAAA;AAAA,IAC1B,CAAA;AAAA,IACA,MAAM,SAAS,GAAA,EAAK;AAClB,MAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,IAAA,CAAK,MAAgB,GAAA,CAAI,QAAA,EAAU,CAAC,GAAG,CAAC,CAAA;AAC/D,MAAA,OAAO,KAAK,CAAC,CAAA,GAAI,WAAW,IAAA,CAAK,CAAC,CAAC,CAAA,GAAI,IAAA;AAAA,IACzC,CAAA;AAAA,IACA,MAAM,SAAS,KAAA,EAAO;AACpB,MAAA,MAAM,KAAK,KAAA,CAAM,GAAA,CAAI,UAAU,MAAA,CAAO,QAAA,CAAS,KAAK,CAAC,CAAA;AAAA,IACvD,CAAA;AAAA,IACA,MAAM,kBAAA,CAAmB,KAAA,EAAO,eAAA,EAAiB;AAC/C,MAAA,MAAM,MAAA,GAAS,oBAAoB,CAAA,GAC/B,MAAM,KAAK,KAAA,CAAM,GAAA,CAAI,SAAA,EAAW,MAAA,CAAO,SAAA,CAAU,KAAK,CAAC,CAAA,GACvD,MAAM,KAAK,KAAA,CAAM,GAAA,CAAI,WAAW,MAAA,CAAO,SAAA,CAAU,KAAA,EAAO,eAAe,CAAC,CAAA;AAC5E,MAAA,OAAA,CAAQ,MAAA,CAAO,YAAY,CAAA,IAAK,CAAA;AAAA,IAClC,CAAA;AAAA,IACA,MAAM,MAAM,MAAA,EAAQ;AAClB,MAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,KAAA,CAAM,IAAI,KAAA,EAAO,CAAC,MAAM,CAAC,CAAA;AACnD,MAAA,OAAO,OAAO,QAAA,IAAY,CAAA;AAAA,IAC5B,CAAA;AAAA,IACA,MAAM,KAAA,GAAQ;AACZ,MAAA,IAAI,KAAA,EAAO,MAAM,IAAA,CAAK,GAAA,EAAI;AAAA,IAC5B;AAAA,GACF;AACF","file":"postgres.cjs","sourcesContent":["/** Output is capped so a chatty job cannot fill the store. The tail is kept. */\nexport const OUTPUT_CAP = 16 * 1024;\n\n/**\n * NUL characters are removed first, since Postgres refuses them in TEXT and\n * JSONB and the whole run row would be lost. The cap then applies to what is\n * left.\n */\nexport function capOutput(text: string): string {\n const clean = stripNul(text);\n if (clean.length <= OUTPUT_CAP) return clean;\n return TRIMMED + clean.slice(clean.length - OUTPUT_CAP);\n}\n\nconst TRIMMED = \"[earlier output trimmed]\\n\";\n\n/**\n * How much text before the kept tail redaction reads, and never keeps: three\n * times the longest secret a default pattern can match (a PEM key's 16 KB body\n * with its header and footer, under OUTPUT_CAP + 1024), since a replacement\n * grows what it replaces at most threefold.\n */\nexport const REDACT_EDGE = 3 * (OUTPUT_CAP + 1024);\n\n/**\n * Output or an error as it is stored: redacted, then capped like capOutput,\n * so the cut cannot fall inside a secret and keep what follows its label.\n * Text of at most OUTPUT_CAP + REDACT_EDGE is redacted whole. Longer text is\n * cut to that many units from its end first, and after redacting, the first\n * REDACT_EDGE units are never kept: a secret whose label fell before that cut\n * is left out with them. NULs go before and after `redact`.\n */\nexport function redactAndCap(text: string, redact: (text: string) => string): string {\n const clean = stripNul(text);\n const from = clean.length - (OUTPUT_CAP + REDACT_EDGE);\n if (from <= 0) return capOutput(redact(clean));\n const redacted = stripNul(redact(clean.slice(from)));\n return TRIMMED + redacted.slice(Math.max(redacted.length - OUTPUT_CAP, REDACT_EDGE));\n}\n\n/** Removes every U+0000. */\nexport function stripNul(text: string): string {\n return text.includes(\"\\u0000\") ? text.replace(/\\u0000/g, \"\") : text;\n}\n\n/**\n * Removes every U+0000 from JSON text, keys and strings alike, by dropping\n * each \\u0000 escape (a NUL can appear in JSON no other way). Escapes are\n * read left to right in pairs, so an escaped backslash followed by \"u0000\"\n * is left as it is.\n */\nexport function stripJsonNul(json: string): string {\n return json.includes(\"\\\\u0000\") ? json.replace(/\\\\(u0000|[\\s\\S])/g, (escape, next: string) => (next === \"u0000\" ? \"\" : escape)) : json;\n}\n\n/** \"Name: message\" and the first five stack frames, capped like output. */\nexport function errorMessage(error: unknown): string {\n return capOutput(describeError(error));\n}\n\n/** \"Name: message\" and the first five stack frames, not capped: see redactAndCap. */\nexport function describeError(error: unknown): string {\n if (error instanceof Error) {\n // The stack repeats the header (over several lines when the message has\n // newlines), so take only its frames.\n const frames = (error.stack ?? \"\").split(\"\\n\").filter((line) => /^\\s+at /.test(line)).slice(0, 5);\n return `${error.name}: ${error.message}${frames.length ? `\\n${frames.join(\"\\n\")}` : \"\"}`;\n }\n if (typeof error === \"string\") return error;\n try {\n return JSON.stringify(error) ?? String(error);\n } catch {\n return String(error);\n }\n}\n\nconst REDACTED = \"[redacted]\";\n\n// Bounded quantifiers throughout, so a long line cannot make these backtrack.\n// They apply in this order, each to the text the ones before it left.\nconst SECRET_PATTERNS: [RegExp, string | ((match: string, ...groups: string[]) => string)][] = [\n // A PEM private key, header to footer. Without a footer (the output was\n // trimmed) it runs to the end of the base64 body. A \"-\" that starts five\n // dashes ends the body, so the footer is never swallowed into it.\n [\n /-----BEGIN (?:[A-Z0-9]{1,20} ){0,3}PRIVATE KEY-----(?:[A-Za-z0-9+/=\\s,:]|-(?!----)){0,16384}(?:-----END (?:[A-Z0-9]{1,20} ){0,3}PRIVATE KEY-----)?/g,\n REDACTED,\n ],\n // password=..., API_KEY: ..., \"client_secret\": \"...\", TOKEN='...', token=...,\n // :password=>\"...\" (but not max_tokens: 800). A quoted value is blanked to\n // its closing quote, spaces and all, and keeps its quotes.\n [\n /\\b([A-Za-z0-9_-]{0,40}(?:secret|token|passw(?:or)?d|pwd|api[_-]?key|access[_-]?key|private[_-]?key|credential)[A-Za-z0-9_-]{0,40}(?<![Tt][Oo][Kk][Ee][Nn][Ss])\"?\\s{0,3}(?:=>|[=:])\\s{0,3})(?:(\")[^\"\\n]{1,4096}\"|(')[^'\\n]{1,4096}'|[\"']?[^\\s\"',;&]{1,4096})/gi,\n (_match, name: string, double?: string, single?: string) => {\n const quote = double ?? single ?? \"\";\n return `${name}${quote}${REDACTED}${quote}`;\n },\n ],\n // Authorization: Basic <base64> and Authorization: Token <token>, also as a JSON or hash entry.\n [/\\b((?:proxy-)?authorization[\"']?\\s{0,3}(?:=>|[=:])\\s{0,3}[\"']?\\s{0,3}(?:basic|token)\\s{1,3})[A-Za-z0-9._~+/=:-]{1,4096}/gi, `$1${REDACTED}`],\n // Credentials inside a URL: postgres://user:password@host. The password\n // runs to the last \"@\" before a \"/\" or a space, so one that contains \"@\"\n // is blanked whole.\n [/(\\b[a-z][a-z0-9+.-]{0,30}:\\/\\/[^\\s/:@]{0,256}:)[^\\s/]{1,256}@/gi, `$1${REDACTED}@`],\n // Authorization: Bearer <token>\n [/\\b(Bearer\\s{1,3})[A-Za-z0-9._~+/=-]{8,4096}/g, `$1${REDACTED}`],\n // A bare JWT: three base64url segments, the first starting eyJ.\n [/\\beyJ[A-Za-z0-9_-]{4,4096}\\.[A-Za-z0-9_-]{4,4096}\\.[A-Za-z0-9_-]{0,4096}/g, REDACTED],\n // Incoming webhook URLs carry their secret in the path.\n [/(\\bhooks\\.slack\\.com\\/(?:services|workflows|triggers)\\/)[A-Za-z0-9/_-]{1,255}/gi, `$1${REDACTED}`],\n [/(\\bdiscord(?:app)?\\.com\\/api\\/(?:v\\d{1,2}\\/)?webhooks\\/)[A-Za-z0-9/_-]{1,255}/gi, `$1${REDACTED}`],\n // Well-known token shapes: AWS, GitHub, Slack, Stripe, Anthropic, OpenAI, and Google style keys.\n [/\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b/g, REDACTED],\n [/\\b(?:gh[pousr]_[A-Za-z0-9]{30,255}|github_pat_[A-Za-z0-9_]{20,255})\\b/g, REDACTED],\n [/\\bxox[abposr]-[A-Za-z0-9-]{10,255}/g, REDACTED],\n [/\\b[rsp]k_(?:live|test)_[A-Za-z0-9]{10,255}\\b/g, REDACTED],\n [/\\bwhsec_[A-Za-z0-9+/=]{16,255}/g, REDACTED],\n [/\\bsk-[A-Za-z0-9_-]{20,255}/g, REDACTED],\n [/\\bAIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])/g, REDACTED],\n];\n\n/**\n * The default `redact`: blanks values that look like secrets (key=value pairs\n * with secret-ish names, Authorization headers, URL credentials, bearer\n * tokens, JWTs, PEM private keys, webhook URLs, and well-known token formats)\n * before output or an error is stored, shown, or sent anywhere.\n */\nexport function redactSecrets(text: string): string {\n let out = text;\n for (const [pattern, replacement] of SECRET_PATTERNS) out = out.replace(pattern, replacement as string);\n return out;\n}\n","import { stripJsonNul, stripNul } from \"../output.js\";\nimport 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, as stateVersion() reads it: a whole\n // number from 0 to 2^53 - 1, else 0 (none, or a foreign row's 1.5 or \"x\",\n // which must neither fail the statement nor refuse every write for good;\n // on SQLite, also text that is not JSON).\n // Each CASE tests the JSON type before any cast.\n const version = (column: string) => {\n if (pg) {\n const v = `(${column}->>'version')::numeric`;\n return `CASE WHEN jsonb_typeof(${column}->'version') <> 'number' THEN 0 WHEN ${v} % 1 = 0 AND ${v} BETWEEN 0 AND 9007199254740991 THEN ${v}::bigint ELSE 0 END`;\n }\n const v = `json_extract(${column}, '$.version')`;\n // Text that is not JSON at all (SQLite holds any) counts as 0 too, before json_type could fail on it.\n return `CASE WHEN NOT json_valid(${column}) THEN 0 WHEN json_type(${column}, '$.version') NOT IN ('integer', 'real') THEN 0 WHEN ${v} = CAST(${v} AS INTEGER) AND ${v} BETWEEN 0 AND 9007199254740991 THEN CAST(${v} AS INTEGER) ELSE 0 END`;\n };\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/**\n * Postgres refuses U+0000 in TEXT and JSONB, and a refused write loses the\n * whole row, so every dialect writes text without it: a run's trigger,\n * output, error, and metric names, and every key and string of a definition\n * and a state. Identifiers (a job's name, a run's id) are written as given;\n * the client refuses one with a NUL before it gets here.\n */\nconst text = (value: string | null) => (value === null ? null : stripNul(value));\nconst jsonText = (value: unknown) => stripJsonNul(JSON.stringify(value));\n\n// Parameters in statement order, so both drivers bind the same values.\nexport const params = {\n upsertJob: (definition: StoredJobDefinition, now: number) => [definition.name, jsonText(definition), now, now],\n insertRun: (run: Run) => [\n run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), stripNul(run.trigger),\n ],\n updateRun: (run: Run) => [run.status, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), run.id],\n updateRunIf: (run: Run, fromStatuses: Run[\"status\"][]) => [\n run.status, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), run.id, ...fromStatuses,\n ],\n setState: (state: JobState) => [state.job, jsonText(state)],\n casInsert: (state: JobState) => [state.job, jsonText(state)],\n casUpdate: (state: JobState, expectedVersion: number) => [jsonText(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\n/**\n * Rows are read leniently: a foreign, hand-edited, or damaged row (SQLite\n * keeps whatever type it is given, in any column) must affect only its own\n * job, never every read. JSON text that does not parse reads as null, which\n * the client takes as no state, or as an unreadable definition it reports.\n */\nconst json = (v: unknown): unknown => {\n if (typeof v !== \"string\") return v;\n try {\n return JSON.parse(v) as unknown;\n } catch {\n return null;\n }\n};\n/** A time, or a count of milliseconds, as a column holds it (a string from Postgres's BIGINT): NaN when it is not a number. */\nconst number = (v: unknown): number => (typeof v === \"number\" ? v : typeof v === \"string\" && v.trim() !== \"\" ? Number(v) : Number.NaN);\n/** A time that must be there: one that is not a finite number reads as 0. */\nconst time = (v: unknown): number => {\n const n = number(v);\n return Number.isFinite(n) ? n : 0;\n};\n/** A time or duration that may be absent: one that is not a finite number reads as null. */\nconst maybeTime = (v: unknown): number | null => {\n const n = number(v);\n return Number.isFinite(n) ? n : null;\n};\nconst textOrNull = (v: unknown): string | null => (typeof v === \"string\" ? v : null);\nconst isObject = (v: unknown): v is Record<string, number> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n\n/** A job's row. Its definition is as stored, or null when its text does not parse; the client reads the rest (see readStoredJob). */\nexport function rowToJob(r: JobRow): StoredJob {\n return { name: r.name, definition: json(r.definition) as StoredJobDefinition, createdAt: time(r.created_at), updatedAt: time(r.updated_at) };\n}\n\n/**\n * A run's row. A start that is not a finite number reads as 0, a finish or\n * duration as null; an error or output that is not text as null; metrics\n * that do not parse to an object as {}; a trigger that is not text as \"run\".\n */\nexport function rowToRun(r: RunRow): Run {\n const metrics = json(r.metrics);\n return {\n id: r.id, job: r.job, status: r.status, startedAt: time(r.started_at), finishedAt: maybeTime(r.finished_at),\n durationMs: maybeTime(r.duration_ms), error: textOrNull(r.error), output: textOrNull(r.output),\n metrics: isObject(metrics) ? metrics : {}, trigger: typeof r.trigger === \"string\" ? r.trigger : \"run\",\n };\n}\n\n/** A state's row, or null when its text does not parse (normalizeState reads anything else that is not an object as no state). */\nexport function rowToState(r: StateRow): JobState {\n return json(r.state) as JobState;\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 /**\n * Bring your own pool instead, with its own timeouts (see `poolOptions`\n * for the two this store sets on a pool it makes). It is not closed by close(), and it needs an\n * 'error' listener of its own (`pool.on(\"error\", ...)`): pg emits one when an\n * idle connection is lost (the database restarted, a proxy closed it), and\n * Node ends the process for an 'error' event nothing listens to.\n */\n pool?: pg.Pool;\n /** Table name prefix: lowercase letters, digits, and underscores. Default \"cronwatch_\". */\n prefix?: string;\n /**\n * Options for the pool this store makes, passed to `new pg.Pool()` as they\n * are (`max`, `ssl`, `idleTimeoutMillis`, `statement_timeout`, and the\n * rest). Two have defaults here that pg does not have, so the store\n * cannot wait on the database for ever: `connectionTimeoutMillis` is 5000\n * (pg waits without limit for a connection, so an unreachable database or\n * a pool with every connection busy would hold a write until something\n * else gave way), and `keepAlive` is true (TCP keepalive, so a connection\n * whose other end went away without a word is noticed and dropped rather\n * than waited on). Give either here to change it; 0 for\n * `connectionTimeoutMillis` is pg's own wait without limit. Not used with\n * `pool`: a pool you pass in is left exactly as you made it.\n */\n poolOptions?: pg.PoolConfig;\n /**\n * Called when the pool this store made loses an idle connection (the\n * database restarted or failed over, a proxy closed it). The pool has\n * already dropped that connection and the next query opens another, so\n * there is nothing to do but hear of it; pass the client's onError to see\n * these with the rest. Not used with `pool`. Default: ignored.\n */\n onError?: (error: unknown) => void;\n}\n\n/** How long the store's own pool waits for a connection, unless `poolOptions` says otherwise. */\nconst CONNECT_TIMEOUT_MS = 5_000;\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 & Required<Pick<Store, \"lastRun\">> {\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({\n connectionTimeoutMillis: CONNECT_TIMEOUT_MS,\n keepAlive: true,\n ...options.poolOptions,\n connectionString: options.connectionString ?? options.poolOptions?.connectionString ?? process.env.DATABASE_URL,\n });\n if (owned) {\n // pg emits 'error' on the pool when an idle connection is lost, and an\n // 'error' event with no listener is thrown, which would end the app. The\n // pool has dropped the connection by now; the next query opens another.\n pool.on(\"error\", (error) => {\n try {\n options.onError?.(error);\n } catch {\n // An onError that throws must not become the uncaught exception this listener is here to stop.\n }\n });\n }\n\n // A connection whose ROLLBACK fails (it died part way) is destroyed, not\n // handed back to the pool, and the error that stopped the work is the one thrown.\n const transaction = async (work: (client: pg.PoolClient) => Promise<void>) => {\n const client = await pool.connect();\n let broken: Error | undefined;\n try {\n await client.query(\"BEGIN\");\n await work(client);\n await client.query(\"COMMIT\");\n } catch (e) {\n try {\n await client.query(\"ROLLBACK\");\n } catch (rollback) {\n broken = rollback instanceof Error ? rollback : new Error(String(rollback));\n }\n throw e;\n } finally {\n client.release(broken);\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"]}