@rebasepro/server 0.25.0-canary.ge06257e → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/{GCSStorageController-CAKa_EFW.js → GCSStorageController-CRd9bFrc.js} +3 -3
  2. package/dist/{GCSStorageController-CAKa_EFW.js.map → GCSStorageController-CRd9bFrc.js.map} +1 -1
  3. package/dist/{S3StorageController-Kt-LJpf3.js → S3StorageController-D00EFa5P.js} +3 -3
  4. package/dist/{S3StorageController-Kt-LJpf3.js.map → S3StorageController-D00EFa5P.js.map} +1 -1
  5. package/dist/api/closed-database.d.ts +25 -0
  6. package/dist/api/errors.d.ts +6 -10
  7. package/dist/{auth-xk37vrHO.js → auth-Dkvo2tTJ.js} +43 -35
  8. package/dist/auth-Dkvo2tTJ.js.map +1 -0
  9. package/dist/backend/boot-announcements.d.ts +13 -0
  10. package/dist/backend/create-backend.d.ts +4 -0
  11. package/dist/{backup-CpaJWXSc.js → backup-B9m3cb3X.js} +7 -7
  12. package/dist/{backup-CpaJWXSc.js.map → backup-B9m3cb3X.js.map} +1 -1
  13. package/dist/boot/boot.d.ts +10 -0
  14. package/dist/{contract-routes-DiFwiLg2.js → contract-routes-CsFXW7j4.js} +2 -2
  15. package/dist/{contract-routes-DiFwiLg2.js.map → contract-routes-CsFXW7j4.js.map} +1 -1
  16. package/dist/{cron-loader-NHqRfKF0.js → cron-loader-Dj6lA4HQ.js} +2 -2
  17. package/dist/{cron-loader-NHqRfKF0.js.map → cron-loader-Dj6lA4HQ.js.map} +1 -1
  18. package/dist/{cron-routes-CPIhdasM.js → cron-routes-CwbLZguO.js} +5 -5
  19. package/dist/{cron-routes-CPIhdasM.js.map → cron-routes-CwbLZguO.js.map} +1 -1
  20. package/dist/{cron-scheduler-D1DT0nw5.js → cron-scheduler-B04ACoUE.js} +2 -2
  21. package/dist/{cron-scheduler-D1DT0nw5.js.map → cron-scheduler-B04ACoUE.js.map} +1 -1
  22. package/dist/{cron-store-DEpVstUU.js → cron-store-DkyfuLOH.js} +3 -3
  23. package/dist/{cron-store-DEpVstUU.js.map → cron-store-DkyfuLOH.js.map} +1 -1
  24. package/dist/{ddl-bootstrap-BNqi2NZe.js → ddl-bootstrap-Ck20GSIX.js} +2 -2
  25. package/dist/{ddl-bootstrap-BNqi2NZe.js.map → ddl-bootstrap-Ck20GSIX.js.map} +1 -1
  26. package/dist/{errors-CHSJYpoy.js → errors-OMWkKSH-.js} +76 -22
  27. package/dist/errors-OMWkKSH-.js.map +1 -0
  28. package/dist/{function-loader-DbCSoNJj.js → function-loader-KKDqTcIr.js} +2 -2
  29. package/dist/{function-loader-DbCSoNJj.js.map → function-loader-KKDqTcIr.js.map} +1 -1
  30. package/dist/{function-routes-C0emkHtX.js → function-routes-RxrCHNuM.js} +2 -2
  31. package/dist/{function-routes-C0emkHtX.js.map → function-routes-RxrCHNuM.js.map} +1 -1
  32. package/dist/functions/index.js +18 -1
  33. package/dist/functions/index.js.map +1 -1
  34. package/dist/{history-recorder-RZ1Ow4YY.js → history-recorder-B2rkuhuy.js} +2 -2
  35. package/dist/{history-recorder-RZ1Ow4YY.js.map → history-recorder-B2rkuhuy.js.map} +1 -1
  36. package/dist/index.es.js +632 -129
  37. package/dist/index.es.js.map +1 -1
  38. package/dist/{jobs-8G-_agld.js → jobs-h1WRq3cp.js} +3 -3
  39. package/dist/{jobs-8G-_agld.js.map → jobs-h1WRq3cp.js.map} +1 -1
  40. package/dist/{jwt-YhciR93H.js → jwt-D3oFauZI.js} +2 -2
  41. package/dist/{jwt-YhciR93H.js.map → jwt-D3oFauZI.js.map} +1 -1
  42. package/dist/{logger-Bt32sHFw.js → logger-CW8pmTS6.js} +35 -3
  43. package/dist/logger-CW8pmTS6.js.map +1 -0
  44. package/dist/{logs-routes-BM2cDory.js → logs-routes-CBt7mMIL.js} +3 -3
  45. package/dist/{logs-routes-BM2cDory.js.map → logs-routes-CBt7mMIL.js.map} +1 -1
  46. package/dist/{openapi-generator-IKwDAQGO.js → openapi-generator-BjHr6upl.js} +2 -2
  47. package/dist/{openapi-generator-IKwDAQGO.js.map → openapi-generator-BjHr6upl.js.map} +1 -1
  48. package/dist/{proxy-BEKbsil6.js → proxy-CH8Jsovg.js} +2 -2
  49. package/dist/{proxy-BEKbsil6.js.map → proxy-CH8Jsovg.js.map} +1 -1
  50. package/dist/{query-parser-BnhugNwZ.js → query-parser-otFZqJsC.js} +3 -3
  51. package/dist/{query-parser-BnhugNwZ.js.map → query-parser-otFZqJsC.js.map} +1 -1
  52. package/dist/{request-timeout-LiGpCCP1.js → request-timeout-59crS-9u.js} +3 -3
  53. package/dist/{request-timeout-LiGpCCP1.js.map → request-timeout-59crS-9u.js.map} +1 -1
  54. package/dist/{schema-editor-routes-zjVKreVQ.js → schema-editor-routes-Bsut0bCC.js} +2 -2
  55. package/dist/{schema-editor-routes-zjVKreVQ.js.map → schema-editor-routes-Bsut0bCC.js.map} +1 -1
  56. package/dist/{soft-delete-params-uHxJoETb.js → soft-delete-params-BmlVwecC.js} +2 -2
  57. package/dist/{soft-delete-params-uHxJoETb.js.map → soft-delete-params-BmlVwecC.js.map} +1 -1
  58. package/dist/{sql-export-routes-D86u7N9i.js → sql-export-routes-DmB_uZ4l.js} +3 -3
  59. package/dist/{sql-export-routes-D86u7N9i.js.map → sql-export-routes-DmB_uZ4l.js.map} +1 -1
  60. package/dist/{upload-validation-pXcDy_EQ.js → upload-validation-Bgo626uY.js} +2 -2
  61. package/dist/{upload-validation-pXcDy_EQ.js.map → upload-validation-Bgo626uY.js.map} +1 -1
  62. package/dist/utils/dev-dependency-command.d.ts +15 -0
  63. package/dist/utils/dev-port.d.ts +8 -0
  64. package/dist/utils/logger.d.ts +24 -0
  65. package/package.json +5 -5
  66. package/dist/auth-xk37vrHO.js.map +0 -1
  67. package/dist/errors-CHSJYpoy.js.map +0 -1
  68. package/dist/logger-Bt32sHFw.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"cron-scheduler-D1DT0nw5.js","names":[],"sources":["../../types/src/types/env_boolean.ts","../src/cron/scale-to-zero.ts","../src/cron/cron-scheduler.ts"],"sourcesContent":["const YES = new Set([\"true\", \"1\", \"yes\", \"on\"]);\nconst NO = new Set([\"false\", \"0\", \"no\", \"off\"]);\n\n/**\n * Read a boolean environment variable — one spelling rule for every reader.\n *\n * `true`, `1`, `yes` and `on` are true; `false`, `0`, `no` and `off` are\n * false. Case and surrounding whitespace are ignored. Anything else — unset,\n * blank, or a value that spells neither — is `undefined`, so the caller's\n * default decides and a typo lands on the side the caller chose:\n *\n * ```ts\n * parseEnvBoolean(env.FORCE_LOCAL_STORAGE) === true // off unless said\n * parseEnvBoolean(env.REBASE_MCP_OPEN_REGISTRATION) !== false // on unless said\n * ```\n *\n * It exists because every reader used to spell this itself, and they\n * disagreed. The reader that mattered tested the raw string for truthiness —\n * `!process.env.FORCE_LOCAL_STORAGE` — and every non-empty string is truthy,\n * so `FORCE_LOCAL_STORAGE=false`, written to say \"there is no durable volume\n * here\", switched the production storage guard off and sent uploads to a\n * container filesystem the next redeploy erased. Beside it, `=== \"true\"` made\n * `DISABLE_DB_ROLE_SWITCHING=1` do nothing and `!== \"false\"` made\n * `REBASE_MCP_OPEN_REGISTRATION=0` leave registration open.\n *\n * The boot schemas in `@rebasepro/server` (`loadEnv`, `loadBootEnv`) accept a\n * strict subset — `true`, `false` and blank — and refuse any other value\n * before the server starts. On that subset they read exactly what this reads,\n * so a variable a schema declares and something else reads lazily cannot mean\n * two different things.\n *\n * It lives in this package, with no dependencies, for the reason\n * {@link storageEnvSuffix} does: the runtime, its drivers and the CLI all read\n * these variables, and a second parser is a second chance to disagree.\n *\n * @group Models\n */\nexport function parseEnvBoolean(value: string | undefined): boolean | undefined {\n if (value === undefined) return undefined;\n const spelled = value.trim().toLowerCase();\n if (YES.has(spelled)) return true;\n if (NO.has(spelled)) return false;\n return undefined;\n}\n","/**\n * Scale-to-zero detection for the cron scheduler.\n *\n * The scheduler drives jobs with in-process `setTimeout`. That works on any\n * always-running instance, but on a platform that freezes or evicts the\n * container between requests (Cloud Run with `--min-instances=0`, AWS Lambda,\n * Vercel functions) the timers simply never fire — the process boots, logs the\n * jobs as registered, and silently runs nothing.\n *\n * None of these platforms expose their scaling floor to the container, so this\n * detection is a heuristic: it identifies the *platform*, not the setting. It\n * is a warning only — it must never influence boot.\n *\n * Environment variables used here were verified against vendor documentation:\n * - `K_SERVICE` / `K_REVISION` / `K_CONFIGURATION` — Cloud Run services\n * (Cloud Run container contract; no variable exposes min-instances).\n * - `CLOUD_RUN_JOB` — Cloud Run jobs (same contract).\n * - `AWS_LAMBDA_FUNCTION_NAME` — reserved AWS Lambda runtime variable.\n * - `VERCEL=1` — Vercel system environment variable, available at runtime.\n * - `KUBERNETES_SERVICE_HOST` — injected into every pod by the kubelet. Used\n * as an *exclusion*: a Deployment pod runs continuously, and Knative on\n * Kubernetes also sets `K_SERVICE`, so a pod is never warned about.\n */\n\nimport { parseEnvBoolean } from \"@rebasepro/types\";\n\n/** Environment variable that permanently silences the scale-to-zero warning. */\nexport const CRON_ALWAYS_ON_ENV = \"REBASE_CRON_ALWAYS_ON\";\n\n/** The subset of `process.env` this module reads. */\nexport type EnvLike = Record<string, string | undefined>;\n\nexport interface FreezableRuntime {\n /** Human-readable platform name, used verbatim in the warning. */\n platform: string;\n /** Names of the environment variables that identified the platform. */\n signals: string[];\n}\n\n/** Minimal shape of a registered job needed to build the warning. */\nexport interface WarnableJob {\n id: string;\n enabled: boolean;\n}\n\nexport interface ScaleToZeroWarning {\n message: string;\n data: Record<string, unknown>;\n}\n\n/**\n * Identify a runtime whose instances can be frozen or torn down between\n * requests. Returns `undefined` when the platform is unknown or known to run\n * continuously.\n */\nexport function detectFreezableRuntime(env: EnvLike = process.env): FreezableRuntime | undefined {\n // A Kubernetes pod (GKE, EKS, self-hosted) runs continuously. Knative and\n // Cloud Run for Anthos set K_SERVICE *inside* a pod, so this exclusion has\n // to come first or every Knative pod would be a false positive.\n if (env.KUBERNETES_SERVICE_HOST) return undefined;\n\n if (env.K_SERVICE) {\n const signals = [\"K_SERVICE\"];\n if (env.K_REVISION) signals.push(\"K_REVISION\");\n if (env.K_CONFIGURATION) signals.push(\"K_CONFIGURATION\");\n return { platform: \"Cloud Run\", signals };\n }\n\n if (env.CLOUD_RUN_JOB) {\n return { platform: \"Cloud Run Jobs\", signals: [\"CLOUD_RUN_JOB\"] };\n }\n\n if (env.AWS_LAMBDA_FUNCTION_NAME) {\n return { platform: \"AWS Lambda\", signals: [\"AWS_LAMBDA_FUNCTION_NAME\"] };\n }\n\n if (parseEnvBoolean(env.VERCEL) === true) {\n return { platform: \"Vercel\", signals: [\"VERCEL\"] };\n }\n\n return undefined;\n}\n\n/** How many job ids to name before collapsing the rest into \"+N more\". */\nconst MAX_NAMED_JOBS = 10;\n\n/**\n * Build the boot-time warning, or `undefined` when it does not apply.\n *\n * Fires only when all of the following hold:\n * 1. `NODE_ENV=production` — a laptop or CI run is not at risk.\n * 2. At least one *enabled* job is registered — nothing to lose otherwise.\n * 3. The environment looks like a freezable platform (see above).\n * 4. `REBASE_CRON_ALWAYS_ON` is not set to a truthy value.\n */\nexport function buildScaleToZeroWarning(\n jobs: WarnableJob[],\n env: EnvLike = process.env\n): ScaleToZeroWarning | undefined {\n if (env.NODE_ENV !== \"production\") return undefined;\n if (parseEnvBoolean(env[CRON_ALWAYS_ON_ENV]) === true) return undefined;\n\n const enabled = jobs.filter((job) => job.enabled).map((job) => job.id);\n if (enabled.length === 0) return undefined;\n\n const runtime = detectFreezableRuntime(env);\n if (!runtime) return undefined;\n\n const named = enabled.slice(0, MAX_NAMED_JOBS);\n const list = enabled.length > named.length\n ? `${named.join(\", \")} (+${enabled.length - named.length} more)`\n : named.join(\", \");\n\n const message = `[cron] ${runtime.platform} detected — in-process timers do not fire while an instance is frozen or scaled to zero, so ${enabled.length} enabled job(s) may never run: ${list}; drive them from an external scheduler instead (POST /api/cron/:id/trigger, e.g. Cloud Scheduler). ${runtime.platform} does not expose its scaling floor to the container, so an always-warm deployment cannot be confirmed from inside the process — set ${CRON_ALWAYS_ON_ENV}=1 to silence this if at least one instance is pinned warm.`;\n\n return {\n message,\n data: {\n platform: runtime.platform,\n signals: runtime.signals,\n jobs: enabled\n }\n };\n}\n","import type {\n CronJobDefinition,\n CronJobStatus,\n CronJobLogEntry,\n CronJobRunState,\n CronJobContext,\n RejectedCronJob\n} from \"@rebasepro/types\";\nimport type { RebaseServerClient } from \"@rebasepro/types\";\nimport { hostname } from \"node:os\";\nimport { randomUUID } from \"node:crypto\";\nimport type { LoadedCronJob } from \"./cron-loader\";\nimport type { CronJobRunSummary, CronStore } from \"./cron-store\";\nimport { logger, redactSensitiveText, stringifyForLog } from \"../utils/logger.js\";\nimport { buildScaleToZeroWarning } from \"./scale-to-zero.js\";\n\n// ─── Cron expression parser (minimal, no external dependency) ────────\n// Supports standard 5-field cron (minute hour dom month dow).\n// Returns the next Date after `after` that matches the expression.\n\n/**\n * Expand a single cron field into an ordered array of allowed values.\n * Supports: `*`, `N`, `N-M`, `N/S`, `N-M/S`, `*\\/S`, and comma-separated combinations.\n */\nfunction expandCronField(field: string, min: number, max: number): number[] {\n const results = new Set<number>();\n for (const segment of field.split(\",\")) {\n const trimmed = segment.trim();\n if (trimmed === \"*\") {\n for (let i = min; i <= max; i++) results.add(i);\n } else if (trimmed.includes(\"/\")) {\n const [rangeStr, stepStr] = trimmed.split(\"/\");\n const step = parseInt(stepStr, 10);\n if (isNaN(step) || step <= 0) {\n throw new Error(`Invalid step value \"${stepStr}\" in cron field \"${field}\"`);\n }\n let start = min;\n let end = max;\n if (rangeStr !== \"*\") {\n if (rangeStr.includes(\"-\")) {\n const [a, b] = rangeStr.split(\"-\").map(Number);\n start = a;\n end = b;\n } else {\n start = parseInt(rangeStr, 10);\n }\n }\n for (let i = start; i <= end; i += step) results.add(i);\n } else if (trimmed.includes(\"-\")) {\n const [a, b] = trimmed.split(\"-\").map(Number);\n for (let i = a; i <= b; i++) results.add(i);\n } else {\n const val = parseInt(trimmed, 10);\n if (isNaN(val)) {\n throw new Error(`Invalid value \"${trimmed}\" in cron field \"${field}\"`);\n }\n results.add(val);\n }\n }\n return [...results].sort((a, b) => a - b);\n}\n\n/**\n * Validates a standard 5-field cron expression structurally and semantically.\n * Returns `{ valid: true }` or `{ valid: false, reason: string }`.\n */\nexport function validateCronExpression(schedule: string): { valid: true } | { valid: false; reason: string } {\n if (!schedule || typeof schedule !== \"string\") {\n return { valid: false,\nreason: \"Schedule must be a non-empty string\" };\n }\n const parts = schedule.trim().split(/\\s+/);\n if (parts.length !== 5) {\n return { valid: false,\nreason: `Expected 5 fields, got ${parts.length}` };\n }\n const fieldRanges: [string, number, number][] = [\n [\"minute\", 0, 59],\n [\"hour\", 0, 23],\n [\"day of month\", 1, 31],\n [\"month\", 1, 12],\n [\"day of week\", 0, 6]\n ];\n for (let i = 0; i < 5; i++) {\n const [name, min, max] = fieldRanges[i];\n try {\n const values = expandCronField(parts[i], min, max);\n if (values.length === 0) {\n return { valid: false,\nreason: `${name} field \"${parts[i]}\" produces no values` };\n }\n for (const v of values) {\n if (v < min || v > max) {\n return { valid: false,\nreason: `${name} field value ${v} out of range [${min}–${max}]` };\n }\n }\n } catch (err) {\n return { valid: false,\nreason: `${name} field: ${err instanceof Error ? err.message : String(err)}` };\n }\n }\n return { valid: true };\n}\n\n/** The five cron fields, pre-expanded into the values each one allows. */\ninterface CronFields {\n minutes: number[];\n hours: number[];\n doms: number[];\n months: number[];\n dows: number[];\n}\n\n/** Expand all five fields of an expression. Throws on invalid expressions. */\nfunction parseCronFields(expression: string): CronFields {\n const parts = expression.trim().split(/\\s+/);\n if (parts.length < 5) {\n throw new Error(`Invalid cron expression: \"${expression}\". Expected 5 fields.`);\n }\n const [minField, hourField, domField, monField, dowField] = parts;\n return {\n minutes: expandCronField(minField, 0, 59),\n hours: expandCronField(hourField, 0, 23),\n doms: expandCronField(domField, 1, 31),\n months: expandCronField(monField, 1, 12),\n dows: expandCronField(dowField, 0, 6) // 0=Sunday\n };\n}\n\n/**\n * Whether an IANA zone name is one this runtime can read a schedule in.\n *\n * `Intl` is the authority: it throws a RangeError on a name it does not know,\n * which is the only check that tracks the tz database the process actually\n * ships. A misspelled zone must fail when the job loads, not silently read\n * the schedule as local time.\n */\nexport function isValidTimeZone(zone: string): boolean {\n try {\n new Intl.DateTimeFormat(\"en-US\", { timeZone: zone });\n return true;\n } catch {\n return false;\n }\n}\n\n/** Wall-clock parts of an instant, in a zone, as the cron fields see them. */\ninterface WallClock {\n year: number;\n minute: number;\n hour: number;\n dom: number;\n month: number;\n dow: number;\n}\n\nconst formatters = new Map<string, Intl.DateTimeFormat>();\nconst DOW: Record<string, number> = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };\n\nfunction wallClockIn(candidate: Date, zone: string): WallClock {\n let formatter = formatters.get(zone);\n if (!formatter) {\n formatter = new Intl.DateTimeFormat(\"en-US\", {\n timeZone: zone,\n hourCycle: \"h23\",\n weekday: \"short\",\n year: \"numeric\",\n month: \"numeric\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"numeric\"\n });\n formatters.set(zone, formatter);\n }\n const parts: Record<string, string> = {};\n for (const part of formatter.formatToParts(candidate)) parts[part.type] = part.value;\n return {\n year: Number(parts.year),\n minute: Number(parts.minute),\n hour: Number(parts.hour),\n dom: Number(parts.day),\n month: Number(parts.month),\n dow: DOW[parts.weekday] ?? candidate.getDay()\n };\n}\n\n/**\n * The wall clock an instant reads in `zone` when one is named, else in the\n * process's own zone — which is whatever the host is set to, UTC in nearly\n * every container. A schedule that names its zone means the same wall-clock\n * hour on every host it runs on.\n */\nfunction wallClockOf(instantMs: number, zone?: string): WallClock {\n const candidate = new Date(instantMs);\n if (zone) return wallClockIn(candidate, zone);\n return {\n year: candidate.getFullYear(),\n minute: candidate.getMinutes(),\n hour: candidate.getHours(),\n dom: candidate.getDate(),\n month: candidate.getMonth() + 1, // getMonth is 0-11\n dow: candidate.getDay()\n };\n}\n\n/**\n * The zone's offset from UTC at an instant: its wall clock read back as UTC,\n * minus the instant. Two instants with the same offset have wall clocks\n * exactly as far apart as the instants themselves.\n */\nfunction offsetAt(instantMs: number, wall: WallClock): number {\n return Date.UTC(wall.year, wall.month - 1, wall.dom, wall.hour, wall.minute) - instantMs;\n}\n\n/** Whether a wall-clock day — month, day of month, weekday — can hold a slot. */\nfunction dayMatches(wall: WallClock, fields: CronFields): boolean {\n return fields.months.includes(wall.month)\n && fields.doms.includes(wall.dom)\n && fields.dows.includes(wall.dow);\n}\n\n/** Whether a wall-clock minute matches every field of the expression. */\nfunction wallMatches(wall: WallClock, fields: CronFields): boolean {\n return dayMatches(wall, fields)\n && fields.hours.includes(wall.hour)\n && fields.minutes.includes(wall.minute);\n}\n\nconst MINUTES_PER_DAY = 1440;\n\n/**\n * How many wall-clock minutes from a non-matching `wall` to the first later\n * wall minute that could match. Never past the next wall midnight, so the\n * minutes in between share `wall`'s day and every one of them is a miss.\n */\nfunction wallMinutesToNextCandidate(wall: WallClock, fields: CronFields): number {\n const minuteOfDay = wall.hour * 60 + wall.minute;\n if (!dayMatches(wall, fields)) return MINUTES_PER_DAY - minuteOfDay;\n if (!fields.hours.includes(wall.hour)) {\n const nextHour = fields.hours.find(hour => hour > wall.hour);\n return nextHour === undefined ? MINUTES_PER_DAY - minuteOfDay : nextHour * 60 - minuteOfDay;\n }\n const nextMinute = fields.minutes.find(minute => minute > wall.minute);\n return nextMinute === undefined ? 60 - wall.minute : nextMinute - wall.minute;\n}\n\n/**\n * The mirror of {@link wallMinutesToNextCandidate}: how many wall-clock\n * minutes back from a non-matching `wall` to the latest earlier wall minute\n * that could match. Never before 23:59 of the previous wall day.\n */\nfunction wallMinutesToPreviousCandidate(wall: WallClock, fields: CronFields): number {\n const minuteOfDay = wall.hour * 60 + wall.minute;\n if (!dayMatches(wall, fields)) return minuteOfDay + 1;\n if (!fields.hours.includes(wall.hour)) {\n const previousHour = [...fields.hours].reverse().find(hour => hour < wall.hour);\n return previousHour === undefined ? minuteOfDay + 1 : minuteOfDay - (previousHour * 60 + 59);\n }\n const previousMinute = [...fields.minutes].reverse().find(minute => minute < wall.minute);\n return previousMinute === undefined ? wall.minute + 1 : wall.minute - previousMinute;\n}\n\n/**\n * How far forward to look for the next matching slot.\n *\n * Four years and a day, not one year. `0 0 29 2 *` — run on 29 February — is a\n * legitimate expression whose slot can be almost four years out, and a one-year\n * search never found it.\n */\nconst MAX_SLOT_SEARCH_MINUTES = 4 * 525960 + 1440;\n\nconst MINUTE_MS = 60_000;\n\n/**\n * The first real minute in `[startMs, endMs)`, stepping in `direction`, whose\n * wall clock matches `fields` — the same answer as testing every minute in\n * turn, without testing every minute.\n *\n * The search moves through *real* minutes and only reads the wall clock, never\n * steps it: across a daylight-saving change a wall-clock minute is not a real\n * one. Falling back, a wall-clock step from 02:59 summer time lands on 03:00\n * winter time and skips the repeated hour; inside it, the \"next\" minute can be\n * an hour in the past. That is why this used to test one real minute at a\n * time — and a yearly job named in a zone cost half a million `Intl` calls,\n * blocking the event loop for a third of a second on every re-arm, and two\n * seconds for 29 February.\n *\n * Now it hops. From a miss, {@link wallMinutesToNextCandidate} says how many\n * wall minutes cannot match (the rest of a day that is the wrong date, the\n * rest of an hour that is the wrong hour, the minutes before the next listed\n * one). While the zone's offset holds, that many wall minutes are exactly\n * that many real minutes, so the hop is taken when the offset at both ends is\n * the same. When it differs, a transition lies inside the hop: the search\n * bisects for the first minute on the new offset and resumes there — every\n * minute before it is on the old offset, so its wall clock is inside the\n * stretch already known to miss. A hop is at most a day, and the one thing\n * this assumes is that no zone changes its offset and changes it back within\n * a single day, which none in the tz database does. Each day costs one\n * wall-clock read, plus about a dozen for each transition crossed.\n *\n * `direction: -1` walks back from `startMs` to `endMs` (inclusive) with the\n * mirror of the same hop, for {@link findMostRecentSlot}.\n */\nfunction searchSlot(\n fields: CronFields,\n startMs: number,\n endMs: number,\n direction: 1 | -1,\n zone?: string\n): number | undefined {\n const inWindow = (instantMs: number) => direction === 1 ? instantMs < endMs : instantMs >= endMs;\n let current = startMs;\n let wall = wallClockOf(current, zone);\n while (inWindow(current)) {\n if (wallMatches(wall, fields)) return current;\n const skip = direction === 1\n ? wallMinutesToNextCandidate(wall, fields)\n : wallMinutesToPreviousCandidate(wall, fields);\n const target = current + direction * skip * MINUTE_MS;\n const targetWall = wallClockOf(target, zone);\n const offset = offsetAt(current, wall);\n if (skip === 1 || offsetAt(target, targetWall) === offset) {\n current = target;\n wall = targetWall;\n continue;\n }\n // The offset changes inside the hop. Bisect for the first minute, in\n // the direction of travel, that is no longer on `offset`.\n let held = current;\n let changed = target;\n let changedWall = targetWall;\n while (Math.abs(changed - held) > MINUTE_MS) {\n const middle = held + direction * Math.floor(Math.abs(changed - held) / MINUTE_MS / 2) * MINUTE_MS;\n const middleWall = wallClockOf(middle, zone);\n if (offsetAt(middle, middleWall) === offset) {\n held = middle;\n } else {\n changed = middle;\n changedWall = middleWall;\n }\n }\n current = changed;\n wall = changedWall;\n }\n return undefined;\n}\n\n/**\n * Calculate the next Date after `after` that matches the cron expression.\n * Throws on invalid expressions.\n */\nexport function parseCronExpression(expression: string, after: Date, timezone?: string): Date {\n const fields = parseCronFields(expression);\n\n // From `after + 1 minute`, in real minutes — see `searchSlot`.\n const start = new Date(after);\n start.setUTCSeconds(0, 0);\n const startMs = start.getTime() + MINUTE_MS;\n const slot = searchSlot(fields, startMs, startMs + MAX_SLOT_SEARCH_MINUTES * MINUTE_MS, 1, timezone);\n if (slot !== undefined) return new Date(slot);\n\n // No slot inside the window. Refuse rather than invent one.\n //\n // This used to return `after + 1 minute`, which is indistinguishable from a\n // schedule that really does fire every minute — so an expression with no\n // reachable slot ran sixty times an hour, forever. `0 0 29 2 *` was caught\n // by it while the search window was a single year: a job meant to run once\n // every four years became the busiest job on the deployment.\n //\n // The caller schedules inside a `try` and reports a job it could not\n // schedule, which is the correct outcome for an expression that names no\n // time. Genuinely impossible dates (`0 0 31 2 *`) land here too, and should.\n throw new Error(\n `Cron expression \"${expression}\" has no matching time within ` +\n `${Math.round(MAX_SLOT_SEARCH_MINUTES / 525960)} years of ${after.toISOString()}. ` +\n \"Check the day-of-month and month fields — a date such as 31 February never occurs.\"\n );\n}\n\n/**\n * The latest slot matching `expression` within the inclusive window\n * `[from, to]`, or `undefined` when the expression has no slot in it.\n *\n * Walks backwards from `to`, so the first hit is already the answer — in the\n * common case (a job that ran normally moments ago) that is a handful of\n * steps, not a scan of the whole window.\n *\n * `to`'s own minute is included: an instance booting at 06:00:30 has *not* run\n * the 06:00 slot — `parseCronExpression` already skipped past it to tomorrow —\n * so that slot is genuinely missed and must be a candidate.\n *\n * Seconds and milliseconds are zeroed to match how `parseCronExpression`\n * builds a slot, so the same wall-clock slot serialises to a byte-identical\n * ISO string down either path. The claim key depends on that.\n */\nexport function findMostRecentSlot(expression: string, from: Date, to: Date, timezone?: string): Date | undefined {\n const fields = parseCronFields(expression);\n\n // Real minutes, as in `parseCronExpression`: a local-time step back from\n // 02:00 winter time lands on 01:59 summer time and walks over the repeated\n // hour.\n const start = new Date(to);\n start.setUTCSeconds(0, 0);\n const startMs = start.getTime();\n const floorMs = Math.max(from.getTime(), startMs - (MAX_SLOT_SEARCH_MINUTES - 1) * MINUTE_MS);\n const slot = searchSlot(fields, startMs, floorMs, -1, timezone);\n return slot === undefined ? undefined : new Date(slot);\n}\n\n// ─── In-memory ring buffer for logs ──────────────────────────────────\n\nconst MAX_LOGS_PER_JOB = 50;\n\n/**\n * What one run keeps of its `ctx.log` lines: up to 500 lines or 64 KB,\n * whichever comes first. The lines past that are counted, and one line at the\n * end says how many there were; every line still reaches the server log.\n *\n * Uncapped, a job logging one line per item kept them all — in the 50-run ring\n * per job, in a `cron_logs` row, and in every history read: a per-minute sync\n * of 1,000 items held 4 MB per job in memory and wrote 115 MB a day.\n */\nexport const MAX_CAPTURED_LOG_LINES = 500;\nexport const MAX_CAPTURED_LOG_BYTES = 64 * 1024;\n\n/**\n * A run's `ctx.log` capture, capped at {@link MAX_CAPTURED_LOG_LINES} and\n * {@link MAX_CAPTURED_LOG_BYTES}. The kept lines are the first ones: once one\n * line is dropped, every later one is too, so the history reads as a prefix of\n * the run's output and never as lines with gaps between them.\n */\nfunction createLogCapture(firstLine?: string) {\n const lines: string[] = [];\n let bytes = 0;\n let dropped = 0;\n let closed = false;\n const capture = {\n add(line: string): void {\n if (closed) return;\n const size = Buffer.byteLength(line, \"utf8\");\n if (dropped === 0 && lines.length < MAX_CAPTURED_LOG_LINES && bytes + size <= MAX_CAPTURED_LOG_BYTES) {\n lines.push(line);\n bytes += size;\n return;\n }\n dropped++;\n },\n /** The lines kept, and the line that says what was not. Later `add`s are ignored. */\n close(): string[] {\n if (!closed && dropped > 0) {\n lines.push(`… ${dropped} more line${dropped === 1 ? \"\" : \"s\"} not kept — see the server log`);\n }\n closed = true;\n return lines;\n }\n };\n if (firstLine !== undefined) capture.add(firstLine);\n return capture;\n}\n\n/**\n * Minimum milliseconds between scheduled executions of the same job.\n * Prevents tight re-execution loops caused by jitter or clock drift.\n */\nconst MIN_SCHEDULE_INTERVAL_MS = 5_000; // 5 seconds\n\n/**\n * Largest delay setTimeout can hold. Node stores it in a 32-bit signed int;\n * anything larger silently clamps to 1ms and fires immediately, so a slot\n * further out than this must be reached in hops rather than one timer.\n */\nconst MAX_TIMER_DELAY_MS = 2_147_483_647; // 2^31 - 1, ~24.8 days\n\n/**\n * `setTimeout` for a delay that may exceed {@link MAX_TIMER_DELAY_MS}: armed in\n * hops no longer than the ceiling, re-measured against the deadline on each\n * wake. Returns the function that cancels it.\n */\nfunction setLongTimeout(callback: () => void, delayMs: number): () => void {\n const deadline = Date.now() + delayMs;\n let handle: ReturnType<typeof setTimeout>;\n const arm = () => {\n handle = setTimeout(() => {\n if (Date.now() < deadline) arm();\n else callback();\n }, Math.min(Math.max(deadline - Date.now(), 0), MAX_TIMER_DELAY_MS));\n };\n arm();\n return () => clearTimeout(handle);\n}\n\n/** A run's timeout when its definition names none. */\nconst DEFAULT_TIMEOUT_SECONDS = 300;\n\n/**\n * How often a started scheduler deletes slot claims and run history past\n * retention. Both accrue one row per job per fire, so a scheduler that lives\n * for months needs to prune while it runs, not only when it next boots.\n */\nconst CLAIM_SWEEP_INTERVAL_MS = 60 * 60_000;\n\n/**\n * How long a run lease outlasts the run's own timeout.\n *\n * The lease is timed on the database's clock from the moment it is taken, the\n * timeout on this process's from a moment later; without a margin a lease\n * could lapse a beat before its run is aborted and released, and another\n * process start the job alongside it.\n */\nconst LEASE_GRACE_SECONDS = 30;\n\n/**\n * The lease a run with no timeout (`timeoutSeconds: Infinity`) holds.\n *\n * A lease is released when its run ends, so this only matters in two cases.\n * The holder crashed: the job is blocked — every trigger answered \"already\n * executing\", every slot skipped — until the lease lapses, so it must lapse\n * within an hour rather than never. The run is still going after an hour:\n * from then on another process may start the job beside it. A job that\n * legitimately runs for hours should say so with a finite `timeoutSeconds`,\n * which its lease then follows.\n */\nconst MAX_UNBOUNDED_LEASE_SECONDS = 3600;\n\n/**\n * What {@link CronScheduler.startJob} answers: the run began at `startedAt`\n * (its history entry will carry the same timestamp), or it did not start and\n * `log` is the skip that says why.\n */\nexport type StartedCronRun =\n | { started: true; startedAt: string }\n | { started: false; log: CronJobLogEntry };\n\n/**\n * Whether a log entry records a run that did not happen because one was\n * already executing — on this process, or on another that holds the job's run\n * lease. The shape is the documented marker of a skip, the one the Studio\n * panel reads.\n */\nexport function isAlreadyExecutingSkip(entry: CronJobLogEntry): boolean {\n const result = entry.result;\n return typeof result === \"object\" && result !== null\n && \"skipped\" in result && result.skipped === true\n && \"reason\" in result && result.reason === \"already_executing\";\n}\n\n/**\n * Why `timeoutSeconds` cannot be used, or `undefined` when it can. Infinity is\n * a timeout that never fires; zero, a negative number or NaN would fail every\n * run the moment it started.\n */\nfunction invalidTimeoutReason(timeoutSeconds: number | undefined): string | undefined {\n if (timeoutSeconds === undefined || timeoutSeconds === Infinity) return undefined;\n if (typeof timeoutSeconds === \"number\" && Number.isFinite(timeoutSeconds) && timeoutSeconds > 0) return undefined;\n return `timeoutSeconds must be a positive number of seconds, or Infinity for none — got ${String(timeoutSeconds)}`;\n}\n\n// ─── CronScheduler ───────────────────────────────────────────────────\n\ninterface RegisteredJob {\n id: string;\n definition: CronJobDefinition;\n enabled: boolean;\n state: CronJobRunState;\n lastRunAt?: Date;\n nextRunAt?: Date;\n lastDurationMs?: number;\n lastError?: string;\n totalRuns: number;\n totalFailures: number;\n timerId?: ReturnType<typeof setTimeout>;\n logs: CronJobLogEntry[];\n /** True while a handler is actively executing (prevents concurrent runs). */\n executing: boolean;\n /**\n * The next slot a listing showed, on a process whose scheduler is not\n * started — see {@link CronScheduler.listedNextRun}.\n */\n listedSlot?: ListedSlot;\n}\n\n/**\n * A next slot computed for a listing, with what it was computed from: the\n * schedule, the zone, and the instant it was computed at.\n */\ninterface ListedSlot {\n schedule: string;\n timezone?: string;\n computedAt: number;\n slot: Date;\n}\n\n/**\n * What this process learned from the store about a job, for a listing: another\n * process running it, and — on a process whose scheduler is not started — its\n * run history and the slot the scheduler will fire next.\n */\ninterface FleetView {\n runningElsewhere?: boolean;\n summary?: CronJobRunSummary;\n nextRunAt?: Date;\n}\n\n/**\n * Whether a run may go ahead. `token` is the lease to release when it ends —\n * absent when no lease was taken, because the store has none or could not\n * answer.\n */\ntype LeaseOutcome =\n | { acquired: true; token?: string }\n | { acquired: false; holder?: string };\n\n/** A job the scheduler refused, with the definition it was refused from. */\ninterface RefusedJob {\n job: RejectedCronJob;\n definition: CronJobDefinition;\n}\n\nexport class CronScheduler {\n private jobs = new Map<string, RegisteredJob>();\n private rejected = new Map<string, RefusedJob>();\n private started = false;\n private store?: CronStore;\n private client?: RebaseServerClient;\n /**\n * Work `stop()` has to wait for: every run executing right now, and every\n * run's log write still on its way to the store.\n */\n private inFlight = new Set<Promise<unknown>>();\n /** The controller that ends each executing run — what `stop()` aborts. */\n private runControllers = new Set<AbortController>();\n /**\n * This process, as a run lease names it: what \"already executing on …\"\n * tells an operator. Each run adds its own suffix, so a release can only\n * ever clear the lease that run took.\n */\n private readonly instanceLabel = `${hostname()}:${process.pid}`;\n /** The hourly claim- and log-retention sweep, while the scheduler is started with a store. */\n private claimSweepTimer?: ReturnType<typeof setInterval>;\n private sweepingClaims = false;\n private sweepingLogs = false;\n\n /**\n * Set the server singleton to make it available to cron job handlers.\n *\n * `RebaseServerClient`, not `RebaseClient`: the object `init.ts` passes is\n * the same one it registers as the singleton, so this was always the true\n * type — and the wider annotation is what let `ctx.client.data` look like a\n * user-scoped plane inside a cron, when it is the admin-scoped one.\n */\n setClient(client: RebaseServerClient): void {\n this.client = client;\n }\n\n /**\n * Attach a persistence store for cron logs.\n * When set, execution logs are written to the database after each run,\n * and counters are seeded from the database on start.\n */\n setStore(store: CronStore): void {\n this.store = store;\n }\n\n /**\n * Register a batch of loaded cron jobs.\n *\n * If the scheduler is already started, newly registered jobs are\n * automatically scheduled (so late-registered jobs don't sit idle).\n *\n * Validates the cron schedule on registration — invalid schedules\n * are rejected with a warning and the job is NOT registered.\n */\n registerJobs(loadedJobs: LoadedCronJob[]): void {\n for (const loaded of loadedJobs) {\n // Validate schedule up-front — reject invalid schedules\n const validation = validateCronExpression(loaded.definition.schedule);\n if (!validation.valid) {\n logger.error(`[cron] Rejecting job \"${loaded.id}\": invalid schedule \"${loaded.definition.schedule}\" — ${validation.reason}`);\n // Kept, not just logged. A rejected job is absent from\n // `listJobs()`, so from the Studio panel it is indistinguishable\n // from a file that was never written — and the commonest reason\n // to land here is a 6-field expression copied from a tool that\n // supports seconds, which is a one-character fix nobody could\n // see without boot-log access.\n this.refuse(loaded, validation.reason);\n continue;\n }\n // Rejected, not read as local time: a misspelled zone that fell\n // back silently would fire at the wrong hour on every host and\n // look like a scheduler bug. Kept in `rejected` for the same reason\n // a bad schedule is — otherwise the panel cannot tell it from a\n // file nobody wrote.\n if (loaded.definition.timezone !== undefined && !isValidTimeZone(loaded.definition.timezone)) {\n const reason =\n `unknown timezone \"${loaded.definition.timezone}\" — ` +\n 'use an IANA name such as \"Europe/Madrid\" or \"UTC\"';\n logger.error(`[cron] Rejecting job \"${loaded.id}\": ${reason}.`);\n this.refuse(loaded, reason);\n continue;\n }\n // Same treatment: a timeout that fails every run the moment it\n // starts is a configuration error, not a run-time one.\n const timeoutProblem = invalidTimeoutReason(loaded.definition.timeoutSeconds);\n if (timeoutProblem) {\n logger.error(`[cron] Rejecting job \"${loaded.id}\": ${timeoutProblem}.`);\n this.refuse(loaded, timeoutProblem);\n continue;\n }\n // A re-register that now validates clears the earlier complaint.\n this.rejected.delete(loaded.id);\n\n const existing = this.jobs.get(loaded.id);\n if (existing) {\n logger.warn(`[cron] Duplicate cron job id: \"${loaded.id}\". Overwriting.`);\n this.stopJob(loaded.id);\n }\n\n const enabled = loaded.definition.enabled !== false;\n\n const job: RegisteredJob = {\n id: loaded.id,\n definition: loaded.definition,\n enabled,\n state: enabled ? \"idle\" : \"disabled\",\n totalRuns: 0,\n totalFailures: 0,\n logs: [],\n executing: false\n };\n this.jobs.set(loaded.id, job);\n\n // If the scheduler is already running, auto-schedule new jobs\n if (this.started && this.shouldArm(job)) {\n this.scheduleNext(loaded.id);\n }\n }\n }\n\n /**\n * Start the scheduler — begins ticking all enabled jobs.\n */\n start(): void {\n if (this.started) return;\n this.started = true;\n\n // Seed counters from DB (non-blocking — scheduler starts immediately)\n if (this.store) {\n this.store.fetchJobStats().then((stats) => {\n for (const [jobId, data] of stats) {\n const job = this.jobs.get(jobId);\n if (job) {\n job.totalRuns = data.totalRuns;\n job.totalFailures = data.totalFailures;\n if (data.lastRunAt) {\n job.lastRunAt = new Date(data.lastRunAt);\n }\n }\n }\n }).catch((err) => {\n logger.warn(\"[cron] Failed to seed job stats from database\", { error: err });\n });\n }\n\n for (const [id, job] of this.jobs) {\n if (this.shouldArm(job)) {\n this.scheduleNext(id);\n }\n }\n if (!this.store) {\n logger.warn(\"[cron] No cron store attached — runs are uncoordinated; with multiple app instances every instance will execute every job\");\n }\n this.warnIfScaleToZero();\n\n // Recover slots that elapsed while nothing was ticking. Deliberately\n // not awaited: catch-up reaches the database and runs handlers, and\n // boot must not wait on either. Its own errors are contained inside.\n void this.catchUpMissedSlots();\n\n // Off the boot path too, and then hourly. It cannot take a claim the\n // catch-up is asking about: a job's latest claim is never swept. The\n // first log sweep is also what counts a history that predates the run\n // counters — a backlog it works through in batches, not in the boot.\n if (this.store?.sweepClaims || this.store?.sweepLogs) {\n const sweep = () => {\n void this.sweepClaims();\n void this.sweepLogs();\n };\n sweep();\n this.claimSweepTimer = setInterval(sweep, CLAIM_SWEEP_INTERVAL_MS);\n this.claimSweepTimer.unref?.();\n }\n\n logger.info(`⏰ Cron scheduler started with ${this.jobs.size} job(s)`);\n }\n\n /** Delete slot claims past retention. Failures are logged; the next hour tries again. */\n private async sweepClaims(): Promise<void> {\n const store = this.store;\n if (!store?.sweepClaims || this.sweepingClaims) return;\n this.sweepingClaims = true;\n try {\n await store.sweepClaims();\n } catch (err) {\n logger.warn(\"[cron] The claim retention sweep failed; it runs again within the hour\", { error: err });\n } finally {\n this.sweepingClaims = false;\n }\n }\n\n /**\n * Delete run history past retention, and count the runs not counted yet.\n * Failures are logged; the next hour tries again, from where this one\n * stopped — each batch it finished is committed.\n */\n private async sweepLogs(): Promise<void> {\n const store = this.store;\n if (!store?.sweepLogs || this.sweepingLogs) return;\n this.sweepingLogs = true;\n try {\n const { counted } = await store.sweepLogs();\n if (counted > 0) {\n logger.info(`[cron] Counted ${counted} earlier run(s) into the jobs' run totals`);\n }\n } catch (err) {\n logger.warn(\"[cron] The run history sweep failed; it runs again within the hour\", { error: err });\n } finally {\n this.sweepingLogs = false;\n }\n }\n\n /**\n * Stop the scheduler: clear every timer, then wait for the runs executing\n * right now — for at most `timeoutMs` when given.\n *\n * A run still going when the budget runs out has its `ctx.signal` aborted\n * and ends there, recorded as a failure that says why. Its slot stays\n * claimed, so no other instance re-runs it; a handler that must not be cut\n * short should watch its signal and leave its work resumable.\n *\n * Nothing is scheduled after `stop()`, whether or not the returned promise\n * is awaited: the timers are cleared before it first yields.\n */\n async stop(timeoutMs?: number): Promise<void> {\n this.started = false;\n for (const [id] of this.jobs) {\n this.stopJob(id);\n }\n if (this.claimSweepTimer) {\n clearInterval(this.claimSweepTimer);\n this.claimSweepTimer = undefined;\n }\n if (this.inFlight.size === 0) return;\n\n const settled = Promise.allSettled([...this.inFlight]).then(() => true);\n if (timeoutMs === undefined) {\n await settled;\n return;\n }\n let budget: ReturnType<typeof setTimeout> | undefined;\n const finished = await Promise.race([\n settled,\n new Promise<false>((resolve) => {\n budget = setTimeout(() => resolve(false), timeoutMs);\n if (budget && typeof budget === \"object\" && \"unref\" in budget) {\n budget.unref();\n }\n })\n ]);\n clearTimeout(budget);\n if (finished || this.runControllers.size === 0) return;\n\n logger.warn(`[cron] ${this.runControllers.size} run(s) still executing after ${timeoutMs}ms — aborting them`);\n for (const controller of this.runControllers) {\n controller.abort(new Error(\"The server is shutting down; the run was stopped before it finished\"));\n }\n }\n\n /**\n * List all registered jobs with their current status.\n */\n listJobs(): CronJobStatus[] {\n return [...this.jobs.values()].map((job) => this.toStatus(job));\n }\n\n /** Keep a refused job, so the admin surface can say why it never runs. */\n private refuse(loaded: LoadedCronJob, reason: string): void {\n this.rejected.set(loaded.id, {\n job: {\n id: loaded.id,\n name: loaded.definition.name ?? loaded.id,\n schedule: loaded.definition.schedule,\n reason\n },\n definition: loaded.definition\n });\n }\n\n /**\n * Jobs that loaded but whose schedule the scheduler refused.\n *\n * Kept apart from {@link listJobs} because they are not jobs — nothing will\n * run them — but they must be reachable, or \"my cron is missing\" and \"my\n * cron will never fire\" look identical from the admin panel.\n */\n listRejectedJobs(): RejectedCronJob[] {\n return [...this.rejected.values()].map(refused => refused.job);\n }\n\n /**\n * {@link listRejectedJobs}, those whose definition matches — how a surface\n * finds a refused job by what it is, as {@link jobIdsWhere} does for the\n * registered ones.\n */\n rejectedJobsWhere(predicate: (definition: CronJobDefinition) => boolean): RejectedCronJob[] {\n return [...this.rejected.values()].filter(refused => predicate(refused.definition)).map(refused => refused.job);\n }\n\n /**\n * The ids of the registered jobs whose definition matches — how a surface\n * finds a job by what it is rather than by the file it happens to live in.\n */\n jobIdsWhere(predicate: (definition: CronJobDefinition) => boolean): string[] {\n return [...this.jobs.values()].filter(job => predicate(job.definition)).map(job => job.id);\n }\n\n /**\n * Get a single job status by ID.\n */\n getJob(id: string): CronJobStatus | undefined {\n const job = this.jobs.get(id);\n return job ? this.toStatus(job) : undefined;\n }\n\n /**\n * {@link listJobs}, as the fleet sees it rather than this process alone.\n *\n * Reads the store: every job's enabled state — a pause another replica\n * made — whether another process holds its run lease, and each job's run\n * count and last run from `cron_logs`. The counters in this process's\n * memory count only the runs it made: the `api` role runs nothing and\n * would say 0 runs for a job the worker has run a thousand times, and one\n * of two scheduling replicas never sees the slots the other one claimed.\n *\n * A store that cannot answer leaves this process's own view, with a\n * warning: a listing is not worth failing.\n */\n async fetchJobs(): Promise<CronJobStatus[]> {\n const jobs = [...this.jobs.values()];\n const views = await this.readFleet(jobs);\n return jobs.map(job => this.toStatus(job, views.get(job.id)));\n }\n\n /** {@link getJob}, as {@link fetchJobs} reads it. */\n async fetchJob(id: string): Promise<CronJobStatus | undefined> {\n const job = this.jobs.get(id);\n if (!job) return undefined;\n const views = await this.readFleet([job]);\n return this.toStatus(job, views.get(id));\n }\n\n /**\n * Get log entries for a job.\n */\n getJobLogs(id: string, limit?: number): CronJobLogEntry[] {\n const job = this.jobs.get(id);\n if (!job) return [];\n const logs = [...job.logs].reverse(); // newest first\n return limit ? logs.slice(0, limit) : logs;\n }\n\n /**\n * Get log entries for a job from the database (if store is available).\n * Falls back to in-memory logs if no store is configured, or if the store\n * has none — a run whose row is still being written.\n *\n * Throws when the store cannot read the history. This process's memory is\n * not a stand-in for it: it is empty on the `api` role and after a\n * restart, and holds one replica's runs at most.\n */\n async getJobLogsFromDb(id: string, limit?: number): Promise<CronJobLogEntry[]> {\n if (this.store) {\n const dbLogs = await this.store.fetchLogs(id, limit);\n if (dbLogs.length > 0) return dbLogs;\n }\n // Fallback to in-memory\n return this.getJobLogs(id, limit);\n }\n\n /**\n * Enable or disable a job in this process only. `null` returns it to what\n * its code declares.\n *\n * {@link persistJobEnabled} is what the admin API calls: this alone is\n * forgotten on restart and reaches no other replica.\n */\n setJobEnabled(id: string, enabled: boolean | null): CronJobStatus | undefined {\n const job = this.jobs.get(id);\n if (!job) return undefined;\n\n const effective = enabled ?? this.codeEnabled(job);\n this.applyEnabled(job, effective);\n\n if (effective && this.started) {\n this.scheduleNext(id);\n } else if (!this.shouldArm(job)) {\n this.stopJob(id);\n }\n\n return this.toStatus(job);\n }\n\n /**\n * Enable or disable a job for every process that runs it, and keep it that\n * way across restarts and redeploys: `true` or `false` overrides the code's\n * `enabled`, `null` hands the decision back to it.\n *\n * The override is written to the store first, and only then applied here,\n * so a write that fails throws and changes nothing — a pause that only the\n * replica serving the request would honour is the failure this exists to\n * end. Every scheduler reads the override when a slot comes due, before it\n * claims the slot.\n *\n * Without a store that keeps the state (a driver with no SQL, persistence\n * switched off, a custom store) this is {@link setJobEnabled}: local, and\n * gone on restart.\n */\n async persistJobEnabled(id: string, enabled: boolean | null, updatedBy?: string): Promise<CronJobStatus | undefined> {\n if (!this.jobs.has(id)) return undefined;\n if (this.store?.saveEnabledOverride) {\n await this.store.saveEnabledOverride(id, enabled, updatedBy);\n }\n return this.setJobEnabled(id, enabled);\n }\n\n /**\n * Manually trigger a job execution immediately.\n *\n * Returns `undefined` if the job doesn't exist.\n * If the job is currently executing — here, or on another process that\n * holds its run lease — returns the log entry with a `skipped: true`\n * result rather than running concurrently; {@link isAlreadyExecutingSkip}\n * recognises it.\n */\n async triggerJob(id: string): Promise<CronJobLogEntry | undefined> {\n const job = this.jobs.get(id);\n if (!job) return undefined;\n\n // Concurrency guard — don't run two instances simultaneously\n if (job.executing) {\n logger.warn(`[cron] Skipping manual trigger of \"${id}\" — already executing`);\n return this.recordSkip(job, \"already_executing\", true);\n }\n\n return this.executeJob(job, true);\n }\n\n /**\n * Start a job now and answer as soon as the run has begun, not when it\n * ends — the manual trigger's `?async=1`.\n *\n * The synchronous {@link triggerJob} holds its HTTP request open for the\n * whole run, and a proxy in front of the server (many stop at 60 s) cuts\n * a long job's answer while the run carries on. This resolves once the\n * run lease is held and the handler is about to be called, so a run that\n * cannot start — already executing here or on the lease holder — is still\n * answered as the skip it is. The run itself is tracked like any other:\n * `stop()` waits for it, and its outcome lands in the run history, where\n * the entry carries `manual: true` and the `startedAt` returned here.\n *\n * Returns `undefined` if the job doesn't exist.\n */\n async startJob(id: string): Promise<StartedCronRun | undefined> {\n const job = this.jobs.get(id);\n if (!job) return undefined;\n\n if (job.executing) {\n logger.warn(`[cron] Skipping manual trigger of \"${id}\" — already executing`);\n return { started: false, log: this.recordSkip(job, \"already_executing\", true) };\n }\n\n let onStart!: (startedAt: string) => void;\n const begun = new Promise<string>((resolve) => { onStart = resolve; });\n const run = this.executeJob(job, true, undefined, onStart);\n // Whichever comes first: the handler being called, or the run ending\n // without it (a skip). A run that starts settles `begun` first, since\n // `onStart` is called before the handler is awaited.\n const first = await Promise.race([\n begun.then((startedAt) => ({ startedAt })),\n run.then((log) => ({ log }))\n ]);\n if (\"startedAt\" in first) return { started: true, startedAt: first.startedAt };\n return { started: false, log: first.log };\n }\n\n /**\n * Record a run that did not happen because the previous one had not\n * finished.\n *\n * Written to `cron_logs`, not only to the in-memory ring. An overlap is the\n * signature of a job that has outgrown its schedule — the one thing you want\n * to see in the history rather than infer from a gap in it — and until now\n * the scheduled path left no trace at all beyond a warning in the process\n * log, which is gone by the time anyone asks. The manual path wrote a ring\n * entry that vanished on restart.\n *\n * `success: true` is deliberate: nothing failed. The `result.skipped` flag\n * and the reason are what distinguishes it, and the Studio panel reads them.\n */\n private recordSkip(job: RegisteredJob, reason: string, manual: boolean, runningOn?: string): CronJobLogEntry {\n const now = new Date().toISOString();\n const why = reason === \"already_executing\" ? \"the previous run has not finished\" : reason;\n const logEntry: CronJobLogEntry = {\n jobId: job.id,\n startedAt: now,\n finishedAt: now,\n durationMs: 0,\n success: true,\n result: { skipped: true, reason },\n logs: [`Skipped: ${why}${runningOn ? ` — it is running on ${runningOn}` : \"\"}`],\n manual\n };\n\n job.logs.push(logEntry);\n if (job.logs.length > MAX_LOGS_PER_JOB) job.logs.shift();\n\n // Tracked like a run's own write, so a shutdown does not close the pool\n // under it.\n if (this.store) {\n this.track(this.store.insertLog(logEntry).catch((persistErr) => {\n logger.error(`[cron] Failed to persist skip for \"${job.id}\"`, { error: persistErr });\n }));\n }\n\n return logEntry;\n }\n\n // ─── Internal ────────────────────────────────────────────────────\n\n /** What the job's own definition says, before any override. */\n private codeEnabled(job: RegisteredJob): boolean {\n return job.definition.enabled !== false;\n }\n\n /**\n * Whether the enabled state lives in the store, where every process reads\n * it, rather than in this process alone.\n */\n private sharesEnabledState(): boolean {\n return Boolean(this.store?.fetchJobStates && this.store.saveEnabledOverride);\n }\n\n /**\n * Whether a job gets a timer here. A job paused in this process's memory\n * needs none — nothing but this process can resume it. A job whose state\n * the store keeps does, paused or not: another replica may resume it, and\n * reading the state when its slot comes due is how this one finds out.\n */\n private shouldArm(job: RegisteredJob): boolean {\n return job.enabled || this.sharesEnabledState();\n }\n\n /** Set a job's enabled flag, and its state with it unless a run is in progress. */\n private applyEnabled(job: RegisteredJob, enabled: boolean): void {\n job.enabled = enabled;\n if (job.executing) return;\n if (!enabled) job.state = \"disabled\";\n else if (job.state === \"disabled\") job.state = \"idle\";\n }\n\n /**\n * Whether a job may run now: the store's override when it keeps one, else\n * the code's `enabled`.\n *\n * One query per fire, before the slot is claimed — a paused job does not\n * spend its slot. A store that cannot answer falls back to the code's\n * `enabled`, with a warning: a scheduled run fails open, as it does when\n * the claims table cannot answer, because a broken state table must not\n * silently stop every job.\n */\n private async isEnabledNow(job: RegisteredJob): Promise<boolean> {\n if (!this.store?.fetchJobStates || !this.sharesEnabledState()) return job.enabled;\n try {\n const state = (await this.store.fetchJobStates([job.id])).get(job.id);\n const enabled = state?.enabled ?? this.codeEnabled(job);\n this.applyEnabled(job, enabled);\n return enabled;\n } catch (err) {\n const enabled = this.codeEnabled(job);\n logger.warn(\n `[cron] Could not read the enabled state of \"${job.id}\" — falling back to the code's ` +\n `enabled (${enabled}); a pause made from the admin API is not being honoured`,\n { error: err }\n );\n return enabled;\n }\n }\n\n /**\n * How long a run's lease lasts: the run's timeout and a grace, so that a\n * crashed holder frees the job once its run would have been aborted\n * anyway. No timeout gets {@link MAX_UNBOUNDED_LEASE_SECONDS}.\n */\n private leaseSeconds(job: RegisteredJob): number {\n const timeout = job.definition.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;\n return Number.isFinite(timeout) ? timeout + LEASE_GRACE_SECONDS : MAX_UNBOUNDED_LEASE_SECONDS;\n }\n\n /**\n * Take the job's run lease, the cross-process half of \"already\n * executing\". A store without leases, or one that cannot answer, lets the\n * run go ahead unleased — with a warning in the second case — the same way\n * a claim that cannot answer does.\n */\n private async acquireLease(job: RegisteredJob): Promise<LeaseOutcome> {\n if (!this.store?.tryAcquireRunLease) return { acquired: true };\n const token = `${this.instanceLabel}#${randomUUID().slice(0, 8)}`;\n try {\n const lease = await this.store.tryAcquireRunLease(job.id, token, this.leaseSeconds(job));\n return lease.acquired ? { acquired: true, token } : lease;\n } catch (err) {\n logger.warn(\n `[cron] Could not take the run lease for \"${job.id}\" — running without it, ` +\n \"so another process may run it at the same time\",\n { error: err }\n );\n return { acquired: true };\n }\n }\n\n /** Release a lease this run took. A release that fails leaves it to lapse. */\n private async releaseLease(job: RegisteredJob, token: string): Promise<void> {\n try {\n await this.store?.releaseRunLease?.(job.id, token);\n } catch (err) {\n logger.warn(\n `[cron] Could not release the run lease for \"${job.id}\" — it lapses on its own ` +\n `within ${this.leaseSeconds(job)}s`,\n { error: err }\n );\n }\n }\n\n /**\n * What the store says about `jobs` beyond this process — see\n * {@link fetchJobs}. Applies the enabled state it reads, and returns the\n * rest as a view for {@link toStatus}. Never throws.\n */\n private async readFleet(jobs: RegisteredJob[]): Promise<Map<string, FleetView>> {\n const views = new Map<string, FleetView>();\n const viewOf = (id: string): FleetView => {\n let view = views.get(id);\n if (!view) {\n view = {};\n views.set(id, view);\n }\n return view;\n };\n const store = this.store;\n const ids = jobs.map(job => job.id);\n\n if (store?.fetchJobStates && this.sharesEnabledState() && ids.length > 0) {\n try {\n const states = await store.fetchJobStates(ids);\n for (const job of jobs) {\n const state = states.get(job.id);\n this.applyEnabled(job, state?.enabled ?? this.codeEnabled(job));\n if (state?.runningBy && !job.executing) viewOf(job.id).runningElsewhere = true;\n }\n } catch (err) {\n logger.warn(\"[cron] Could not read the jobs' shared state — listing what this process knows\", { error: err });\n }\n }\n\n // Only where nothing is scheduled: a started scheduler has its own next\n // slots.\n if (!this.started) {\n const now = new Date();\n for (const job of jobs) {\n try {\n viewOf(job.id).nextRunAt = this.listedNextRun(job, now);\n } catch {\n // A schedule with no reachable slot has no next run to show.\n }\n }\n }\n // Wherever this process stands. Its own counters are seeded from the\n // store once, at start, and after that count only its own runs:\n // with two replicas scheduling, a slot the other one claimed never\n // reached them, and the card said \"idle, 0 runs\" over a history that\n // listed the failure.\n if (store?.fetchRunSummaries && ids.length > 0) {\n try {\n const summaries = await store.fetchRunSummaries(ids);\n for (const [id, summary] of summaries) viewOf(id).summary = summary;\n } catch (err) {\n logger.warn(\"[cron] Could not read the jobs' run history — listing what this process knows\", { error: err });\n }\n }\n return views;\n }\n\n /**\n * The next slot of `job` after `now`, for a listing on a process whose\n * scheduler is not started — the `api` role, which Studio's Cron panel\n * polls every 15 seconds.\n *\n * Cached on the job. The key is the job's schedule and zone (a re-register\n * replaces the job, and with it the cache); the entry holds while\n * `computedAt <= now < slot`. That is the same answer, not an\n * approximation: the search found no matching minute between\n * `computedAt`'s next minute and `slot`, so from any later instant before\n * `slot` it lands on `slot` again. A clock that moves back, or reaching the\n * slot, recomputes. The cache holds a pure function of the schedule, so\n * there is nothing user- or role-scoped in it.\n */\n private listedNextRun(job: RegisteredJob, now: Date): Date {\n const { schedule, timezone } = job.definition;\n const nowMs = now.getTime();\n const cached = job.listedSlot;\n if (\n cached\n && cached.schedule === schedule\n && cached.timezone === timezone\n && cached.computedAt <= nowMs\n && nowMs < cached.slot.getTime()\n ) {\n return cached.slot;\n }\n const slot = parseCronExpression(schedule, now, timezone);\n job.listedSlot = { schedule, timezone, computedAt: nowMs, slot };\n return slot;\n }\n\n /**\n * Warn once at start when the process looks like it is running on a\n * platform that freezes or evicts instances between requests, where the\n * in-process timers this scheduler relies on never fire.\n *\n * Advisory only: any failure here is swallowed so a detection bug can\n * never take a production boot down.\n */\n private warnIfScaleToZero(): void {\n try {\n const warning = buildScaleToZeroWarning(\n [...this.jobs.values()].map((job) => ({ id: job.id, enabled: job.enabled }))\n );\n if (warning) {\n logger.warn(warning.message, warning.data);\n }\n } catch {\n // Never let the advisory check affect startup.\n }\n }\n\n /**\n * Schedule the next execution for a job.\n *\n * Safety guarantees:\n * 1. Clears any existing timer first (prevents leaked/duplicate timers)\n * 2. Enforces a minimum delay to prevent tight loops from jitter\n * 3. Unref's the timer so it doesn't prevent process exit\n * 4. Re-checks enabled & started state before executing\n * 5. Concurrency guard prevents overlapping handler executions\n */\n private scheduleNext(id: string): void {\n const job = this.jobs.get(id);\n if (!job || !this.started || !this.shouldArm(job)) return;\n\n // Clear any previously scheduled timer to prevent double-firing\n this.stopJob(id);\n\n try {\n const now = new Date();\n const nextRun = parseCronExpression(job.definition.schedule, now, job.definition.timezone);\n job.nextRunAt = nextRun;\n\n const rawDelay = nextRun.getTime() - now.getTime();\n // Enforce a minimum delay to prevent tight re-execution loops\n // from event loop jitter or near-zero setTimeout drift\n const delay = Math.max(rawDelay, MIN_SCHEDULE_INTERVAL_MS);\n\n // A slot past the 32-bit timer ceiling cannot be armed directly:\n // setTimeout would clamp it to 1ms and fire at once, and since the\n // slot is already claimed by then, every wake re-schedules the same\n // overflowing delay — a permanent hot loop, not a late job. Sleep\n // to the ceiling and re-derive the delay on waking instead; the\n // cron expression stays the source of truth across the hops.\n if (delay > MAX_TIMER_DELAY_MS) {\n const hop = setTimeout(() => {\n if (this.started && this.shouldArm(job)) this.scheduleNext(id);\n }, MAX_TIMER_DELAY_MS);\n if (hop && typeof hop === \"object\" && \"unref\" in hop) {\n hop.unref();\n }\n job.timerId = hop;\n return;\n }\n\n const timer = setTimeout(async () => {\n // Re-check state: the scheduler may have been stopped between\n // when we scheduled and when we fire\n if (!this.started) return;\n\n // A timer can wake before its slot: a delay past the 32-bit\n // ceiling, a clock stepped backwards by NTP, a VM resuming from\n // suspend. Claiming on an early wake is unrecoverable — claims\n // are permanent, so the slot would be burned and the real run\n // silently skipped when it came due. Re-derive from the wall\n // clock and re-arm instead; only the fire that is genuinely due\n // may claim.\n if (Date.now() < nextRun.getTime()) {\n this.scheduleNext(id);\n return;\n }\n\n // Paused or resumed — perhaps by another replica, perhaps\n // before this process started. Asked of the store at every\n // fire, and before the claim, so a paused job leaves its slot\n // unspent.\n if (!(await this.isEnabledNow(job))) {\n if (this.started && this.shouldArm(job)) this.scheduleNext(id);\n return;\n }\n if (!this.started) return;\n\n // Concurrency guard: if somehow we're already executing, skip\n if (job.executing) {\n logger.warn(`[cron] Skipping scheduled run of \"${id}\" — still executing from previous run`);\n // Recorded as well as logged: a job that keeps overlapping\n // has outgrown its schedule, and that is visible in the run\n // history or nowhere.\n this.recordSkip(job, \"already_executing\", false);\n // Re-schedule to try again later\n this.scheduleNext(id);\n return;\n }\n\n // Cross-instance guard: claim the scheduled slot in the store.\n // The slot is the scheduled fire time — deterministic across\n // instances — so exactly one instance wins each (job, slot) pair.\n // A store without tryClaimRun (pre-claims custom implementation)\n // runs uncoordinated; a throwing store fails open — either way\n // this callback must never reject, or the job would silently\n // stop rescheduling.\n if (this.store?.tryClaimRun) {\n let claimed = true;\n try {\n claimed = await this.store.tryClaimRun(id, nextRun.toISOString());\n } catch (err) {\n logger.warn(`[cron] Claim check threw for \"${id}\" — running uncoordinated`, { error: err });\n }\n if (!claimed) {\n logger.info(`[cron] Slot ${nextRun.toISOString()} for \"${id}\" claimed by another instance — skipping`);\n if (this.started && this.shouldArm(job)) {\n this.scheduleNext(id);\n }\n return;\n }\n }\n\n // Takes the run lease: a manual trigger in progress on another\n // process — the api role's, say — turns this slot into a\n // recorded skip rather than a second run beside it.\n await this.executeJob(job, false);\n\n // Schedule the next tick (only if still started + armed)\n if (this.started && this.shouldArm(job)) {\n this.scheduleNext(id);\n }\n }, delay);\n\n // Unref the timer so it doesn't prevent Node.js from exiting\n // during graceful shutdown\n if (timer && typeof timer === \"object\" && \"unref\" in timer) {\n timer.unref();\n }\n\n job.timerId = timer;\n } catch (err: unknown) {\n logger.error(`[cron] Failed to schedule \"${id}\"`, { error: err });\n job.state = \"error\";\n job.lastError = err instanceof Error ? err.message : String(err);\n }\n }\n\n /**\n * Run any slot that elapsed while no instance was holding a timer for it.\n *\n * Only jobs that opted in via `catchUpWindowSeconds` are considered, and\n * only their single most recent missed slot — see the field's docs for why\n * both limits are deliberate.\n *\n * The claim is what makes this safe. In the ordinary case — an instance\n * restarting minutes after a slot ran normally — the most recent slot is\n * already claimed, so this costs one `tryClaimRun` per job per boot and\n * does nothing. A slot is only executed when no instance, past or present,\n * ever claimed it.\n *\n * Never throws: a failure here must not take down a scheduler that is\n * otherwise ticking correctly.\n */\n private async catchUpMissedSlots(): Promise<void> {\n // Every job with a window whose state the store may override, not only\n // the ones enabled here: a job the code disables can be resumed from\n // the admin API, and one it enables can be paused there. Each is asked\n // below, before its claim.\n const candidates = [...this.jobs.values()].filter(\n job => this.shouldArm(job) && (job.definition.catchUpWindowSeconds ?? 0) > 0\n );\n if (candidates.length === 0) return;\n\n // A claims-capable store is the whole safety mechanism. Without one,\n // every boot would look like \"this slot never ran\" and an instance\n // recycled twice an hour would re-run the same hourly job twice an\n // hour. Refusing to catch up is the correct degradation.\n if (!this.store?.tryClaimRun) {\n logger.warn(\n `[cron] Catch-up is configured on ${candidates.length} job(s) but no claims-capable store is attached — skipping. ` +\n \"Without claims a restart cannot tell an unrun slot from one the previous instance already ran.\"\n );\n return;\n }\n\n for (const job of candidates) {\n try {\n if (!this.started || job.executing) continue;\n // The persisted pause outlives the deploy that is catching up.\n if (!(await this.isEnabledNow(job))) continue;\n if (!this.started || job.executing) continue;\n\n // Read per job, not once for the pass: the jobs ahead of this\n // one ran their catch-ups first, and this job's own timer may\n // have fired newer slots meanwhile. Its most recent slot is the\n // one as of now, or a stale one runs after newer ones already did.\n const now = new Date();\n const windowSeconds = job.definition.catchUpWindowSeconds!;\n const from = new Date(now.getTime() - windowSeconds * 1000);\n const slot = findMostRecentSlot(job.definition.schedule, from, now, job.definition.timezone);\n if (!slot) continue;\n\n const slotIso = slot.toISOString();\n\n // Same key the scheduled path claims with, so a slot that fired\n // normally is already taken and this is a no-op.\n let claimed: boolean;\n try {\n claimed = await this.store.tryClaimRun(job.id, slotIso);\n } catch (err) {\n // Fail closed, unlike the scheduled path. A missed slot is\n // a recovery, not an obligation — running it against a\n // store that cannot tell us whether it already ran risks a\n // duplicate on every boot.\n logger.warn(`[cron] Catch-up claim threw for \"${job.id}\" — skipping catch-up`, { error: err });\n continue;\n }\n\n if (!claimed) continue;\n\n const lateBy = Math.round((now.getTime() - slot.getTime()) / 1000);\n logger.info(`[cron] Catching up missed slot ${slotIso} for \"${job.id}\" (${lateBy}s late)`);\n\n await this.executeJob(job, false, `⏰ Catch-up run for missed slot ${slotIso} (${lateBy}s late)`);\n } catch (err) {\n logger.error(`[cron] Catch-up failed for \"${job.id}\"`, { error: err });\n }\n }\n }\n\n /**\n * Stop a single job's timer and clear its next run state.\n */\n private stopJob(id: string): void {\n const job = this.jobs.get(id);\n if (job?.timerId) {\n clearTimeout(job.timerId);\n job.timerId = undefined;\n job.nextRunAt = undefined;\n }\n }\n\n /**\n * Execute a job's handler with full isolation and safety.\n *\n * - Sets a concurrency flag to prevent overlapping runs\n * - Wraps handler in a timeout race\n * - Captures all logs, errors, and results\n * - Persists to store (non-blocking) if available\n * - Always restores state even on catastrophic errors\n */\n private executeJob(\n job: RegisteredJob,\n manual: boolean,\n seedLog?: string,\n onStart?: (startedAt: string) => void\n ): Promise<CronJobLogEntry> {\n // Aborted when the run is over before its handler is: the timeout\n // below, or `stop()` giving up on it. Without it the timeout only\n // stopped the scheduler waiting: the handler's `fetch` kept its socket,\n // so a job whose timeout matches its interval leaked one abandoned\n // request per tick while every run was already marked failed.\n const abort = new AbortController();\n this.runControllers.add(abort);\n const run = this.runJob(job, manual, abort, seedLog, onStart);\n this.track(run);\n return run.finally(() => {\n this.runControllers.delete(abort);\n });\n }\n\n /** Hold `stop()` until `work` settles. */\n private track(work: Promise<unknown>): void {\n this.inFlight.add(work);\n const release = () => { this.inFlight.delete(work); };\n work.then(release, release);\n }\n\n private async runJob(\n job: RegisteredJob,\n manual: boolean,\n abort: AbortController,\n seedLog?: string,\n onStart?: (startedAt: string) => void\n ): Promise<CronJobLogEntry> {\n // Checked here as well as by each caller, because each caller awaits\n // something (a claim, a state read) between its own check and this.\n if (job.executing) {\n return this.recordSkip(job, \"already_executing\", manual);\n }\n // Set executing flag — prevents concurrent runs. Taken before the lease\n // is awaited, so a second trigger on this process cannot slip in.\n job.executing = true;\n\n // The lease is what keeps the other processes off: the worker's\n // scheduled run and the api role's manual trigger never saw each\n // other's in-process flag.\n const lease = await this.acquireLease(job);\n if (!lease.acquired) {\n job.executing = false;\n logger.warn(`[cron] Skipping ${manual ? \"manual trigger\" : \"scheduled run\"} of \"${job.id}\" — it is running on ${lease.holder ?? \"another process\"}`);\n return this.recordSkip(job, \"already_executing\", manual, lease.holder ?? \"another process\");\n }\n\n const startedAt = new Date();\n // A caller-supplied first line, stored with the run's own output. A\n // catch-up uses it to say so in the persisted log, where an operator\n // reading `cron_logs` will actually see it — `manual` is the only other\n // provenance the entry carries, and a catch-up is not manual.\n const capturedLogs = createLogCapture(seedLog || undefined);\n\n const ctx: CronJobContext = {\n jobId: job.id,\n scheduledAt: startedAt,\n signal: abort.signal,\n log: (...args: unknown[]) => {\n const line = args.map((a) =>\n typeof a === \"string\" ? a : JSON.stringify(a)\n ).join(\" \");\n // Capped — see `MAX_CAPTURED_LOG_LINES`. The process log below\n // is not: it gets every line.\n capturedLogs.add(line);\n // Also to the process log. `cron_logs` is behind the admin\n // API, so a line that went only there was missing from the\n // platform's logs, which is where an operator looks when a\n // run goes wrong. The values are redacted on the way: a\n // stringified object reaches the logger as text, and the\n // logger's own pass over the message only removes `Failed\n // query:` spans from it.\n logger.info(\n args.map((a) => typeof a === \"string\" ? a : stringifyForLog(a)).join(\" \"),\n { cron: job.id }\n );\n },\n // `rebase`, and only `rebase`: it matches the singleton import and\n // `defineFunction`'s context. The old `client` alias re-exposed\n // `client.data`, the name `RebaseServerClient` deliberately omits so\n // that the RLS-bypassing plane is spelled `dataAsAdmin` everywhere.\n rebase: this.client!\n };\n\n job.state = \"running\";\n job.lastRunAt = startedAt;\n job.totalRuns++;\n onStart?.(startedAt.toISOString());\n\n let success = true;\n let error: string | undefined;\n let result: unknown;\n\n // The run ends when the handler settles or when `abort` fires,\n // whichever is first — so an abort ends the run even for a handler\n // that ignores its signal, and the run is recorded rather than left\n // \"running\" forever.\n const aborted = new Promise<never>((_, reject) => {\n const end = () => reject(abort.signal.reason);\n if (abort.signal.aborted) end();\n else abort.signal.addEventListener(\"abort\", end, { once: true });\n });\n // Handled here as well as by the race: a handler that throws before\n // the race is entered leaves nothing else listening.\n aborted.catch(() => undefined);\n\n try {\n // Race with timeout. Past the 32-bit timer ceiling a plain\n // setTimeout fires after 1ms, so a month-long timeout would fail\n // every run at once; Infinity is no timeout at all.\n const timeout = (job.definition.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1000;\n const cancelTimeout = Number.isFinite(timeout)\n ? setLongTimeout(\n () => abort.abort(new Error(`Cron job \"${job.id}\" timed out after ${timeout}ms`)),\n timeout\n )\n : undefined;\n\n try {\n result = await Promise.race([Promise.resolve(job.definition.handler(ctx)), aborted]);\n } finally {\n cancelTimeout?.();\n }\n } catch (err: unknown) {\n success = false;\n // Redacted at the point of capture, not just on the way to the log:\n // this string is persisted into `cron_logs` and rendered in the\n // Studio cron panel, and a job that fails on a query would\n // otherwise store `Failed query: <sql>\\nparams: <values>` — the\n // statement and every bound value — in a table, indefinitely.\n error = redactSensitiveText(err instanceof Error ? err.message : String(err));\n job.totalFailures++;\n } finally {\n // The lease before the flag: in between, a trigger on this process\n // would pass the flag and then find its own process's lease held.\n if (lease.token) await this.releaseLease(job, lease.token);\n // Always clear executing flag — even on catastrophic errors\n job.executing = false;\n }\n\n const finishedAt = new Date();\n const durationMs = finishedAt.getTime() - startedAt.getTime();\n\n job.state = success ? (job.enabled ? \"idle\" : \"disabled\") : \"error\";\n job.lastDurationMs = durationMs;\n job.lastError = error;\n\n const logEntry: CronJobLogEntry = {\n jobId: job.id,\n startedAt: startedAt.toISOString(),\n finishedAt: finishedAt.toISOString(),\n durationMs,\n success,\n error,\n result: result !== undefined ? result : undefined,\n logs: capturedLogs.close(),\n manual\n };\n\n // Push to ring buffer\n job.logs.push(logEntry);\n if (job.logs.length > MAX_LOGS_PER_JOB) {\n job.logs.shift();\n }\n\n // Persist to database (non-blocking for the run, but tracked, so a\n // shutdown does not close the pool under the write)\n if (this.store) {\n this.track(this.store.insertLog(logEntry).catch((persistErr) => {\n logger.error(`[cron] Failed to persist log for \"${job.id}\"`, { error: persistErr });\n }));\n }\n\n if (success) {\n logger.info(`✅ [cron] \"${job.id}\" completed in ${durationMs}ms`);\n } else {\n logger.error(`❌ [cron] \"${job.id}\" failed in ${durationMs}ms: ${error}`);\n }\n\n return logEntry;\n }\n\n /**\n * A job's status: this process's own record, overlaid with what `view`\n * read from the store. A paused job shows no next run, even where its\n * timer stays armed to read the state again at that slot.\n */\n private toStatus(job: RegisteredJob, view: FleetView = {}): CronJobStatus {\n const summary = view.summary;\n const nextRunAt = job.enabled ? (job.nextRunAt ?? view.nextRunAt) : undefined;\n let state = job.state;\n if (!job.executing) {\n if (view.runningElsewhere) state = \"running\";\n else if (summary && job.enabled) state = summary.lastSuccess === false ? \"error\" : \"idle\";\n }\n return {\n id: job.id,\n // Same fallback the two `listJobs` paths already applied: `name` is\n // optional and the id is the file's own name.\n name: job.definition.name ?? job.id,\n description: job.definition.description,\n schedule: job.definition.schedule,\n enabled: job.enabled,\n state,\n lastRunAt: summary ? summary.lastRunAt : job.lastRunAt?.toISOString(),\n nextRunAt: nextRunAt?.toISOString(),\n lastDurationMs: summary ? summary.lastDurationMs : job.lastDurationMs,\n lastError: summary ? (summary.lastSuccess === false ? summary.lastError : undefined) : job.lastError,\n totalRuns: summary ? summary.totalRuns : job.totalRuns,\n totalFailures: summary ? summary.totalFailures : job.totalFailures\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cron-scheduler-B04ACoUE.js","names":[],"sources":["../../types/src/types/env_boolean.ts","../src/cron/scale-to-zero.ts","../src/cron/cron-scheduler.ts"],"sourcesContent":["const YES = new Set([\"true\", \"1\", \"yes\", \"on\"]);\nconst NO = new Set([\"false\", \"0\", \"no\", \"off\"]);\n\n/**\n * Read a boolean environment variable — one spelling rule for every reader.\n *\n * `true`, `1`, `yes` and `on` are true; `false`, `0`, `no` and `off` are\n * false. Case and surrounding whitespace are ignored. Anything else — unset,\n * blank, or a value that spells neither — is `undefined`, so the caller's\n * default decides and a typo lands on the side the caller chose:\n *\n * ```ts\n * parseEnvBoolean(env.FORCE_LOCAL_STORAGE) === true // off unless said\n * parseEnvBoolean(env.REBASE_MCP_OPEN_REGISTRATION) !== false // on unless said\n * ```\n *\n * It exists because every reader used to spell this itself, and they\n * disagreed. The reader that mattered tested the raw string for truthiness —\n * `!process.env.FORCE_LOCAL_STORAGE` — and every non-empty string is truthy,\n * so `FORCE_LOCAL_STORAGE=false`, written to say \"there is no durable volume\n * here\", switched the production storage guard off and sent uploads to a\n * container filesystem the next redeploy erased. Beside it, `=== \"true\"` made\n * `DISABLE_DB_ROLE_SWITCHING=1` do nothing and `!== \"false\"` made\n * `REBASE_MCP_OPEN_REGISTRATION=0` leave registration open.\n *\n * The boot schemas in `@rebasepro/server` (`loadEnv`, `loadBootEnv`) accept a\n * strict subset — `true`, `false` and blank — and refuse any other value\n * before the server starts. On that subset they read exactly what this reads,\n * so a variable a schema declares and something else reads lazily cannot mean\n * two different things.\n *\n * It lives in this package, with no dependencies, for the reason\n * {@link storageEnvSuffix} does: the runtime, its drivers and the CLI all read\n * these variables, and a second parser is a second chance to disagree.\n *\n * @group Models\n */\nexport function parseEnvBoolean(value: string | undefined): boolean | undefined {\n if (value === undefined) return undefined;\n const spelled = value.trim().toLowerCase();\n if (YES.has(spelled)) return true;\n if (NO.has(spelled)) return false;\n return undefined;\n}\n","/**\n * Scale-to-zero detection for the cron scheduler.\n *\n * The scheduler drives jobs with in-process `setTimeout`. That works on any\n * always-running instance, but on a platform that freezes or evicts the\n * container between requests (Cloud Run with `--min-instances=0`, AWS Lambda,\n * Vercel functions) the timers simply never fire — the process boots, logs the\n * jobs as registered, and silently runs nothing.\n *\n * None of these platforms expose their scaling floor to the container, so this\n * detection is a heuristic: it identifies the *platform*, not the setting. It\n * is a warning only — it must never influence boot.\n *\n * Environment variables used here were verified against vendor documentation:\n * - `K_SERVICE` / `K_REVISION` / `K_CONFIGURATION` — Cloud Run services\n * (Cloud Run container contract; no variable exposes min-instances).\n * - `CLOUD_RUN_JOB` — Cloud Run jobs (same contract).\n * - `AWS_LAMBDA_FUNCTION_NAME` — reserved AWS Lambda runtime variable.\n * - `VERCEL=1` — Vercel system environment variable, available at runtime.\n * - `KUBERNETES_SERVICE_HOST` — injected into every pod by the kubelet. Used\n * as an *exclusion*: a Deployment pod runs continuously, and Knative on\n * Kubernetes also sets `K_SERVICE`, so a pod is never warned about.\n */\n\nimport { parseEnvBoolean } from \"@rebasepro/types\";\n\n/** Environment variable that permanently silences the scale-to-zero warning. */\nexport const CRON_ALWAYS_ON_ENV = \"REBASE_CRON_ALWAYS_ON\";\n\n/** The subset of `process.env` this module reads. */\nexport type EnvLike = Record<string, string | undefined>;\n\nexport interface FreezableRuntime {\n /** Human-readable platform name, used verbatim in the warning. */\n platform: string;\n /** Names of the environment variables that identified the platform. */\n signals: string[];\n}\n\n/** Minimal shape of a registered job needed to build the warning. */\nexport interface WarnableJob {\n id: string;\n enabled: boolean;\n}\n\nexport interface ScaleToZeroWarning {\n message: string;\n data: Record<string, unknown>;\n}\n\n/**\n * Identify a runtime whose instances can be frozen or torn down between\n * requests. Returns `undefined` when the platform is unknown or known to run\n * continuously.\n */\nexport function detectFreezableRuntime(env: EnvLike = process.env): FreezableRuntime | undefined {\n // A Kubernetes pod (GKE, EKS, self-hosted) runs continuously. Knative and\n // Cloud Run for Anthos set K_SERVICE *inside* a pod, so this exclusion has\n // to come first or every Knative pod would be a false positive.\n if (env.KUBERNETES_SERVICE_HOST) return undefined;\n\n if (env.K_SERVICE) {\n const signals = [\"K_SERVICE\"];\n if (env.K_REVISION) signals.push(\"K_REVISION\");\n if (env.K_CONFIGURATION) signals.push(\"K_CONFIGURATION\");\n return { platform: \"Cloud Run\", signals };\n }\n\n if (env.CLOUD_RUN_JOB) {\n return { platform: \"Cloud Run Jobs\", signals: [\"CLOUD_RUN_JOB\"] };\n }\n\n if (env.AWS_LAMBDA_FUNCTION_NAME) {\n return { platform: \"AWS Lambda\", signals: [\"AWS_LAMBDA_FUNCTION_NAME\"] };\n }\n\n if (parseEnvBoolean(env.VERCEL) === true) {\n return { platform: \"Vercel\", signals: [\"VERCEL\"] };\n }\n\n return undefined;\n}\n\n/** How many job ids to name before collapsing the rest into \"+N more\". */\nconst MAX_NAMED_JOBS = 10;\n\n/**\n * Build the boot-time warning, or `undefined` when it does not apply.\n *\n * Fires only when all of the following hold:\n * 1. `NODE_ENV=production` — a laptop or CI run is not at risk.\n * 2. At least one *enabled* job is registered — nothing to lose otherwise.\n * 3. The environment looks like a freezable platform (see above).\n * 4. `REBASE_CRON_ALWAYS_ON` is not set to a truthy value.\n */\nexport function buildScaleToZeroWarning(\n jobs: WarnableJob[],\n env: EnvLike = process.env\n): ScaleToZeroWarning | undefined {\n if (env.NODE_ENV !== \"production\") return undefined;\n if (parseEnvBoolean(env[CRON_ALWAYS_ON_ENV]) === true) return undefined;\n\n const enabled = jobs.filter((job) => job.enabled).map((job) => job.id);\n if (enabled.length === 0) return undefined;\n\n const runtime = detectFreezableRuntime(env);\n if (!runtime) return undefined;\n\n const named = enabled.slice(0, MAX_NAMED_JOBS);\n const list = enabled.length > named.length\n ? `${named.join(\", \")} (+${enabled.length - named.length} more)`\n : named.join(\", \");\n\n const message = `[cron] ${runtime.platform} detected — in-process timers do not fire while an instance is frozen or scaled to zero, so ${enabled.length} enabled job(s) may never run: ${list}; drive them from an external scheduler instead (POST /api/cron/:id/trigger, e.g. Cloud Scheduler). ${runtime.platform} does not expose its scaling floor to the container, so an always-warm deployment cannot be confirmed from inside the process — set ${CRON_ALWAYS_ON_ENV}=1 to silence this if at least one instance is pinned warm.`;\n\n return {\n message,\n data: {\n platform: runtime.platform,\n signals: runtime.signals,\n jobs: enabled\n }\n };\n}\n","import type {\n CronJobDefinition,\n CronJobStatus,\n CronJobLogEntry,\n CronJobRunState,\n CronJobContext,\n RejectedCronJob\n} from \"@rebasepro/types\";\nimport type { RebaseServerClient } from \"@rebasepro/types\";\nimport { hostname } from \"node:os\";\nimport { randomUUID } from \"node:crypto\";\nimport type { LoadedCronJob } from \"./cron-loader\";\nimport type { CronJobRunSummary, CronStore } from \"./cron-store\";\nimport { logger, redactSensitiveText, stringifyForLog } from \"../utils/logger.js\";\nimport { buildScaleToZeroWarning } from \"./scale-to-zero.js\";\n\n// ─── Cron expression parser (minimal, no external dependency) ────────\n// Supports standard 5-field cron (minute hour dom month dow).\n// Returns the next Date after `after` that matches the expression.\n\n/**\n * Expand a single cron field into an ordered array of allowed values.\n * Supports: `*`, `N`, `N-M`, `N/S`, `N-M/S`, `*\\/S`, and comma-separated combinations.\n */\nfunction expandCronField(field: string, min: number, max: number): number[] {\n const results = new Set<number>();\n for (const segment of field.split(\",\")) {\n const trimmed = segment.trim();\n if (trimmed === \"*\") {\n for (let i = min; i <= max; i++) results.add(i);\n } else if (trimmed.includes(\"/\")) {\n const [rangeStr, stepStr] = trimmed.split(\"/\");\n const step = parseInt(stepStr, 10);\n if (isNaN(step) || step <= 0) {\n throw new Error(`Invalid step value \"${stepStr}\" in cron field \"${field}\"`);\n }\n let start = min;\n let end = max;\n if (rangeStr !== \"*\") {\n if (rangeStr.includes(\"-\")) {\n const [a, b] = rangeStr.split(\"-\").map(Number);\n start = a;\n end = b;\n } else {\n start = parseInt(rangeStr, 10);\n }\n }\n for (let i = start; i <= end; i += step) results.add(i);\n } else if (trimmed.includes(\"-\")) {\n const [a, b] = trimmed.split(\"-\").map(Number);\n for (let i = a; i <= b; i++) results.add(i);\n } else {\n const val = parseInt(trimmed, 10);\n if (isNaN(val)) {\n throw new Error(`Invalid value \"${trimmed}\" in cron field \"${field}\"`);\n }\n results.add(val);\n }\n }\n return [...results].sort((a, b) => a - b);\n}\n\n/**\n * Validates a standard 5-field cron expression structurally and semantically.\n * Returns `{ valid: true }` or `{ valid: false, reason: string }`.\n */\nexport function validateCronExpression(schedule: string): { valid: true } | { valid: false; reason: string } {\n if (!schedule || typeof schedule !== \"string\") {\n return { valid: false,\nreason: \"Schedule must be a non-empty string\" };\n }\n const parts = schedule.trim().split(/\\s+/);\n if (parts.length !== 5) {\n return { valid: false,\nreason: `Expected 5 fields, got ${parts.length}` };\n }\n const fieldRanges: [string, number, number][] = [\n [\"minute\", 0, 59],\n [\"hour\", 0, 23],\n [\"day of month\", 1, 31],\n [\"month\", 1, 12],\n [\"day of week\", 0, 6]\n ];\n for (let i = 0; i < 5; i++) {\n const [name, min, max] = fieldRanges[i];\n try {\n const values = expandCronField(parts[i], min, max);\n if (values.length === 0) {\n return { valid: false,\nreason: `${name} field \"${parts[i]}\" produces no values` };\n }\n for (const v of values) {\n if (v < min || v > max) {\n return { valid: false,\nreason: `${name} field value ${v} out of range [${min}–${max}]` };\n }\n }\n } catch (err) {\n return { valid: false,\nreason: `${name} field: ${err instanceof Error ? err.message : String(err)}` };\n }\n }\n return { valid: true };\n}\n\n/** The five cron fields, pre-expanded into the values each one allows. */\ninterface CronFields {\n minutes: number[];\n hours: number[];\n doms: number[];\n months: number[];\n dows: number[];\n}\n\n/** Expand all five fields of an expression. Throws on invalid expressions. */\nfunction parseCronFields(expression: string): CronFields {\n const parts = expression.trim().split(/\\s+/);\n if (parts.length < 5) {\n throw new Error(`Invalid cron expression: \"${expression}\". Expected 5 fields.`);\n }\n const [minField, hourField, domField, monField, dowField] = parts;\n return {\n minutes: expandCronField(minField, 0, 59),\n hours: expandCronField(hourField, 0, 23),\n doms: expandCronField(domField, 1, 31),\n months: expandCronField(monField, 1, 12),\n dows: expandCronField(dowField, 0, 6) // 0=Sunday\n };\n}\n\n/**\n * Whether an IANA zone name is one this runtime can read a schedule in.\n *\n * `Intl` is the authority: it throws a RangeError on a name it does not know,\n * which is the only check that tracks the tz database the process actually\n * ships. A misspelled zone must fail when the job loads, not silently read\n * the schedule as local time.\n */\nexport function isValidTimeZone(zone: string): boolean {\n try {\n new Intl.DateTimeFormat(\"en-US\", { timeZone: zone });\n return true;\n } catch {\n return false;\n }\n}\n\n/** Wall-clock parts of an instant, in a zone, as the cron fields see them. */\ninterface WallClock {\n year: number;\n minute: number;\n hour: number;\n dom: number;\n month: number;\n dow: number;\n}\n\nconst formatters = new Map<string, Intl.DateTimeFormat>();\nconst DOW: Record<string, number> = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };\n\nfunction wallClockIn(candidate: Date, zone: string): WallClock {\n let formatter = formatters.get(zone);\n if (!formatter) {\n formatter = new Intl.DateTimeFormat(\"en-US\", {\n timeZone: zone,\n hourCycle: \"h23\",\n weekday: \"short\",\n year: \"numeric\",\n month: \"numeric\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"numeric\"\n });\n formatters.set(zone, formatter);\n }\n const parts: Record<string, string> = {};\n for (const part of formatter.formatToParts(candidate)) parts[part.type] = part.value;\n return {\n year: Number(parts.year),\n minute: Number(parts.minute),\n hour: Number(parts.hour),\n dom: Number(parts.day),\n month: Number(parts.month),\n dow: DOW[parts.weekday] ?? candidate.getDay()\n };\n}\n\n/**\n * The wall clock an instant reads in `zone` when one is named, else in the\n * process's own zone — which is whatever the host is set to, UTC in nearly\n * every container. A schedule that names its zone means the same wall-clock\n * hour on every host it runs on.\n */\nfunction wallClockOf(instantMs: number, zone?: string): WallClock {\n const candidate = new Date(instantMs);\n if (zone) return wallClockIn(candidate, zone);\n return {\n year: candidate.getFullYear(),\n minute: candidate.getMinutes(),\n hour: candidate.getHours(),\n dom: candidate.getDate(),\n month: candidate.getMonth() + 1, // getMonth is 0-11\n dow: candidate.getDay()\n };\n}\n\n/**\n * The zone's offset from UTC at an instant: its wall clock read back as UTC,\n * minus the instant. Two instants with the same offset have wall clocks\n * exactly as far apart as the instants themselves.\n */\nfunction offsetAt(instantMs: number, wall: WallClock): number {\n return Date.UTC(wall.year, wall.month - 1, wall.dom, wall.hour, wall.minute) - instantMs;\n}\n\n/** Whether a wall-clock day — month, day of month, weekday — can hold a slot. */\nfunction dayMatches(wall: WallClock, fields: CronFields): boolean {\n return fields.months.includes(wall.month)\n && fields.doms.includes(wall.dom)\n && fields.dows.includes(wall.dow);\n}\n\n/** Whether a wall-clock minute matches every field of the expression. */\nfunction wallMatches(wall: WallClock, fields: CronFields): boolean {\n return dayMatches(wall, fields)\n && fields.hours.includes(wall.hour)\n && fields.minutes.includes(wall.minute);\n}\n\nconst MINUTES_PER_DAY = 1440;\n\n/**\n * How many wall-clock minutes from a non-matching `wall` to the first later\n * wall minute that could match. Never past the next wall midnight, so the\n * minutes in between share `wall`'s day and every one of them is a miss.\n */\nfunction wallMinutesToNextCandidate(wall: WallClock, fields: CronFields): number {\n const minuteOfDay = wall.hour * 60 + wall.minute;\n if (!dayMatches(wall, fields)) return MINUTES_PER_DAY - minuteOfDay;\n if (!fields.hours.includes(wall.hour)) {\n const nextHour = fields.hours.find(hour => hour > wall.hour);\n return nextHour === undefined ? MINUTES_PER_DAY - minuteOfDay : nextHour * 60 - minuteOfDay;\n }\n const nextMinute = fields.minutes.find(minute => minute > wall.minute);\n return nextMinute === undefined ? 60 - wall.minute : nextMinute - wall.minute;\n}\n\n/**\n * The mirror of {@link wallMinutesToNextCandidate}: how many wall-clock\n * minutes back from a non-matching `wall` to the latest earlier wall minute\n * that could match. Never before 23:59 of the previous wall day.\n */\nfunction wallMinutesToPreviousCandidate(wall: WallClock, fields: CronFields): number {\n const minuteOfDay = wall.hour * 60 + wall.minute;\n if (!dayMatches(wall, fields)) return minuteOfDay + 1;\n if (!fields.hours.includes(wall.hour)) {\n const previousHour = [...fields.hours].reverse().find(hour => hour < wall.hour);\n return previousHour === undefined ? minuteOfDay + 1 : minuteOfDay - (previousHour * 60 + 59);\n }\n const previousMinute = [...fields.minutes].reverse().find(minute => minute < wall.minute);\n return previousMinute === undefined ? wall.minute + 1 : wall.minute - previousMinute;\n}\n\n/**\n * How far forward to look for the next matching slot.\n *\n * Four years and a day, not one year. `0 0 29 2 *` — run on 29 February — is a\n * legitimate expression whose slot can be almost four years out, and a one-year\n * search never found it.\n */\nconst MAX_SLOT_SEARCH_MINUTES = 4 * 525960 + 1440;\n\nconst MINUTE_MS = 60_000;\n\n/**\n * The first real minute in `[startMs, endMs)`, stepping in `direction`, whose\n * wall clock matches `fields` — the same answer as testing every minute in\n * turn, without testing every minute.\n *\n * The search moves through *real* minutes and only reads the wall clock, never\n * steps it: across a daylight-saving change a wall-clock minute is not a real\n * one. Falling back, a wall-clock step from 02:59 summer time lands on 03:00\n * winter time and skips the repeated hour; inside it, the \"next\" minute can be\n * an hour in the past. That is why this used to test one real minute at a\n * time — and a yearly job named in a zone cost half a million `Intl` calls,\n * blocking the event loop for a third of a second on every re-arm, and two\n * seconds for 29 February.\n *\n * Now it hops. From a miss, {@link wallMinutesToNextCandidate} says how many\n * wall minutes cannot match (the rest of a day that is the wrong date, the\n * rest of an hour that is the wrong hour, the minutes before the next listed\n * one). While the zone's offset holds, that many wall minutes are exactly\n * that many real minutes, so the hop is taken when the offset at both ends is\n * the same. When it differs, a transition lies inside the hop: the search\n * bisects for the first minute on the new offset and resumes there — every\n * minute before it is on the old offset, so its wall clock is inside the\n * stretch already known to miss. A hop is at most a day, and the one thing\n * this assumes is that no zone changes its offset and changes it back within\n * a single day, which none in the tz database does. Each day costs one\n * wall-clock read, plus about a dozen for each transition crossed.\n *\n * `direction: -1` walks back from `startMs` to `endMs` (inclusive) with the\n * mirror of the same hop, for {@link findMostRecentSlot}.\n */\nfunction searchSlot(\n fields: CronFields,\n startMs: number,\n endMs: number,\n direction: 1 | -1,\n zone?: string\n): number | undefined {\n const inWindow = (instantMs: number) => direction === 1 ? instantMs < endMs : instantMs >= endMs;\n let current = startMs;\n let wall = wallClockOf(current, zone);\n while (inWindow(current)) {\n if (wallMatches(wall, fields)) return current;\n const skip = direction === 1\n ? wallMinutesToNextCandidate(wall, fields)\n : wallMinutesToPreviousCandidate(wall, fields);\n const target = current + direction * skip * MINUTE_MS;\n const targetWall = wallClockOf(target, zone);\n const offset = offsetAt(current, wall);\n if (skip === 1 || offsetAt(target, targetWall) === offset) {\n current = target;\n wall = targetWall;\n continue;\n }\n // The offset changes inside the hop. Bisect for the first minute, in\n // the direction of travel, that is no longer on `offset`.\n let held = current;\n let changed = target;\n let changedWall = targetWall;\n while (Math.abs(changed - held) > MINUTE_MS) {\n const middle = held + direction * Math.floor(Math.abs(changed - held) / MINUTE_MS / 2) * MINUTE_MS;\n const middleWall = wallClockOf(middle, zone);\n if (offsetAt(middle, middleWall) === offset) {\n held = middle;\n } else {\n changed = middle;\n changedWall = middleWall;\n }\n }\n current = changed;\n wall = changedWall;\n }\n return undefined;\n}\n\n/**\n * Calculate the next Date after `after` that matches the cron expression.\n * Throws on invalid expressions.\n */\nexport function parseCronExpression(expression: string, after: Date, timezone?: string): Date {\n const fields = parseCronFields(expression);\n\n // From `after + 1 minute`, in real minutes — see `searchSlot`.\n const start = new Date(after);\n start.setUTCSeconds(0, 0);\n const startMs = start.getTime() + MINUTE_MS;\n const slot = searchSlot(fields, startMs, startMs + MAX_SLOT_SEARCH_MINUTES * MINUTE_MS, 1, timezone);\n if (slot !== undefined) return new Date(slot);\n\n // No slot inside the window. Refuse rather than invent one.\n //\n // This used to return `after + 1 minute`, which is indistinguishable from a\n // schedule that really does fire every minute — so an expression with no\n // reachable slot ran sixty times an hour, forever. `0 0 29 2 *` was caught\n // by it while the search window was a single year: a job meant to run once\n // every four years became the busiest job on the deployment.\n //\n // The caller schedules inside a `try` and reports a job it could not\n // schedule, which is the correct outcome for an expression that names no\n // time. Genuinely impossible dates (`0 0 31 2 *`) land here too, and should.\n throw new Error(\n `Cron expression \"${expression}\" has no matching time within ` +\n `${Math.round(MAX_SLOT_SEARCH_MINUTES / 525960)} years of ${after.toISOString()}. ` +\n \"Check the day-of-month and month fields — a date such as 31 February never occurs.\"\n );\n}\n\n/**\n * The latest slot matching `expression` within the inclusive window\n * `[from, to]`, or `undefined` when the expression has no slot in it.\n *\n * Walks backwards from `to`, so the first hit is already the answer — in the\n * common case (a job that ran normally moments ago) that is a handful of\n * steps, not a scan of the whole window.\n *\n * `to`'s own minute is included: an instance booting at 06:00:30 has *not* run\n * the 06:00 slot — `parseCronExpression` already skipped past it to tomorrow —\n * so that slot is genuinely missed and must be a candidate.\n *\n * Seconds and milliseconds are zeroed to match how `parseCronExpression`\n * builds a slot, so the same wall-clock slot serialises to a byte-identical\n * ISO string down either path. The claim key depends on that.\n */\nexport function findMostRecentSlot(expression: string, from: Date, to: Date, timezone?: string): Date | undefined {\n const fields = parseCronFields(expression);\n\n // Real minutes, as in `parseCronExpression`: a local-time step back from\n // 02:00 winter time lands on 01:59 summer time and walks over the repeated\n // hour.\n const start = new Date(to);\n start.setUTCSeconds(0, 0);\n const startMs = start.getTime();\n const floorMs = Math.max(from.getTime(), startMs - (MAX_SLOT_SEARCH_MINUTES - 1) * MINUTE_MS);\n const slot = searchSlot(fields, startMs, floorMs, -1, timezone);\n return slot === undefined ? undefined : new Date(slot);\n}\n\n// ─── In-memory ring buffer for logs ──────────────────────────────────\n\nconst MAX_LOGS_PER_JOB = 50;\n\n/**\n * What one run keeps of its `ctx.log` lines: up to 500 lines or 64 KB,\n * whichever comes first. The lines past that are counted, and one line at the\n * end says how many there were; every line still reaches the server log.\n *\n * Uncapped, a job logging one line per item kept them all — in the 50-run ring\n * per job, in a `cron_logs` row, and in every history read: a per-minute sync\n * of 1,000 items held 4 MB per job in memory and wrote 115 MB a day.\n */\nexport const MAX_CAPTURED_LOG_LINES = 500;\nexport const MAX_CAPTURED_LOG_BYTES = 64 * 1024;\n\n/**\n * A run's `ctx.log` capture, capped at {@link MAX_CAPTURED_LOG_LINES} and\n * {@link MAX_CAPTURED_LOG_BYTES}. The kept lines are the first ones: once one\n * line is dropped, every later one is too, so the history reads as a prefix of\n * the run's output and never as lines with gaps between them.\n */\nfunction createLogCapture(firstLine?: string) {\n const lines: string[] = [];\n let bytes = 0;\n let dropped = 0;\n let closed = false;\n const capture = {\n add(line: string): void {\n if (closed) return;\n const size = Buffer.byteLength(line, \"utf8\");\n if (dropped === 0 && lines.length < MAX_CAPTURED_LOG_LINES && bytes + size <= MAX_CAPTURED_LOG_BYTES) {\n lines.push(line);\n bytes += size;\n return;\n }\n dropped++;\n },\n /** The lines kept, and the line that says what was not. Later `add`s are ignored. */\n close(): string[] {\n if (!closed && dropped > 0) {\n lines.push(`… ${dropped} more line${dropped === 1 ? \"\" : \"s\"} not kept — see the server log`);\n }\n closed = true;\n return lines;\n }\n };\n if (firstLine !== undefined) capture.add(firstLine);\n return capture;\n}\n\n/**\n * Minimum milliseconds between scheduled executions of the same job.\n * Prevents tight re-execution loops caused by jitter or clock drift.\n */\nconst MIN_SCHEDULE_INTERVAL_MS = 5_000; // 5 seconds\n\n/**\n * Largest delay setTimeout can hold. Node stores it in a 32-bit signed int;\n * anything larger silently clamps to 1ms and fires immediately, so a slot\n * further out than this must be reached in hops rather than one timer.\n */\nconst MAX_TIMER_DELAY_MS = 2_147_483_647; // 2^31 - 1, ~24.8 days\n\n/**\n * `setTimeout` for a delay that may exceed {@link MAX_TIMER_DELAY_MS}: armed in\n * hops no longer than the ceiling, re-measured against the deadline on each\n * wake. Returns the function that cancels it.\n */\nfunction setLongTimeout(callback: () => void, delayMs: number): () => void {\n const deadline = Date.now() + delayMs;\n let handle: ReturnType<typeof setTimeout>;\n const arm = () => {\n handle = setTimeout(() => {\n if (Date.now() < deadline) arm();\n else callback();\n }, Math.min(Math.max(deadline - Date.now(), 0), MAX_TIMER_DELAY_MS));\n };\n arm();\n return () => clearTimeout(handle);\n}\n\n/** A run's timeout when its definition names none. */\nconst DEFAULT_TIMEOUT_SECONDS = 300;\n\n/**\n * How often a started scheduler deletes slot claims and run history past\n * retention. Both accrue one row per job per fire, so a scheduler that lives\n * for months needs to prune while it runs, not only when it next boots.\n */\nconst CLAIM_SWEEP_INTERVAL_MS = 60 * 60_000;\n\n/**\n * How long a run lease outlasts the run's own timeout.\n *\n * The lease is timed on the database's clock from the moment it is taken, the\n * timeout on this process's from a moment later; without a margin a lease\n * could lapse a beat before its run is aborted and released, and another\n * process start the job alongside it.\n */\nconst LEASE_GRACE_SECONDS = 30;\n\n/**\n * The lease a run with no timeout (`timeoutSeconds: Infinity`) holds.\n *\n * A lease is released when its run ends, so this only matters in two cases.\n * The holder crashed: the job is blocked — every trigger answered \"already\n * executing\", every slot skipped — until the lease lapses, so it must lapse\n * within an hour rather than never. The run is still going after an hour:\n * from then on another process may start the job beside it. A job that\n * legitimately runs for hours should say so with a finite `timeoutSeconds`,\n * which its lease then follows.\n */\nconst MAX_UNBOUNDED_LEASE_SECONDS = 3600;\n\n/**\n * What {@link CronScheduler.startJob} answers: the run began at `startedAt`\n * (its history entry will carry the same timestamp), or it did not start and\n * `log` is the skip that says why.\n */\nexport type StartedCronRun =\n | { started: true; startedAt: string }\n | { started: false; log: CronJobLogEntry };\n\n/**\n * Whether a log entry records a run that did not happen because one was\n * already executing — on this process, or on another that holds the job's run\n * lease. The shape is the documented marker of a skip, the one the Studio\n * panel reads.\n */\nexport function isAlreadyExecutingSkip(entry: CronJobLogEntry): boolean {\n const result = entry.result;\n return typeof result === \"object\" && result !== null\n && \"skipped\" in result && result.skipped === true\n && \"reason\" in result && result.reason === \"already_executing\";\n}\n\n/**\n * Why `timeoutSeconds` cannot be used, or `undefined` when it can. Infinity is\n * a timeout that never fires; zero, a negative number or NaN would fail every\n * run the moment it started.\n */\nfunction invalidTimeoutReason(timeoutSeconds: number | undefined): string | undefined {\n if (timeoutSeconds === undefined || timeoutSeconds === Infinity) return undefined;\n if (typeof timeoutSeconds === \"number\" && Number.isFinite(timeoutSeconds) && timeoutSeconds > 0) return undefined;\n return `timeoutSeconds must be a positive number of seconds, or Infinity for none — got ${String(timeoutSeconds)}`;\n}\n\n// ─── CronScheduler ───────────────────────────────────────────────────\n\ninterface RegisteredJob {\n id: string;\n definition: CronJobDefinition;\n enabled: boolean;\n state: CronJobRunState;\n lastRunAt?: Date;\n nextRunAt?: Date;\n lastDurationMs?: number;\n lastError?: string;\n totalRuns: number;\n totalFailures: number;\n timerId?: ReturnType<typeof setTimeout>;\n logs: CronJobLogEntry[];\n /** True while a handler is actively executing (prevents concurrent runs). */\n executing: boolean;\n /**\n * The next slot a listing showed, on a process whose scheduler is not\n * started — see {@link CronScheduler.listedNextRun}.\n */\n listedSlot?: ListedSlot;\n}\n\n/**\n * A next slot computed for a listing, with what it was computed from: the\n * schedule, the zone, and the instant it was computed at.\n */\ninterface ListedSlot {\n schedule: string;\n timezone?: string;\n computedAt: number;\n slot: Date;\n}\n\n/**\n * What this process learned from the store about a job, for a listing: another\n * process running it, and — on a process whose scheduler is not started — its\n * run history and the slot the scheduler will fire next.\n */\ninterface FleetView {\n runningElsewhere?: boolean;\n summary?: CronJobRunSummary;\n nextRunAt?: Date;\n}\n\n/**\n * Whether a run may go ahead. `token` is the lease to release when it ends —\n * absent when no lease was taken, because the store has none or could not\n * answer.\n */\ntype LeaseOutcome =\n | { acquired: true; token?: string }\n | { acquired: false; holder?: string };\n\n/** A job the scheduler refused, with the definition it was refused from. */\ninterface RefusedJob {\n job: RejectedCronJob;\n definition: CronJobDefinition;\n}\n\nexport class CronScheduler {\n private jobs = new Map<string, RegisteredJob>();\n private rejected = new Map<string, RefusedJob>();\n private started = false;\n private store?: CronStore;\n private client?: RebaseServerClient;\n /**\n * Work `stop()` has to wait for: every run executing right now, and every\n * run's log write still on its way to the store.\n */\n private inFlight = new Set<Promise<unknown>>();\n /** The controller that ends each executing run — what `stop()` aborts. */\n private runControllers = new Set<AbortController>();\n /**\n * This process, as a run lease names it: what \"already executing on …\"\n * tells an operator. Each run adds its own suffix, so a release can only\n * ever clear the lease that run took.\n */\n private readonly instanceLabel = `${hostname()}:${process.pid}`;\n /** The hourly claim- and log-retention sweep, while the scheduler is started with a store. */\n private claimSweepTimer?: ReturnType<typeof setInterval>;\n private sweepingClaims = false;\n private sweepingLogs = false;\n\n /**\n * Set the server singleton to make it available to cron job handlers.\n *\n * `RebaseServerClient`, not `RebaseClient`: the object `init.ts` passes is\n * the same one it registers as the singleton, so this was always the true\n * type — and the wider annotation is what let `ctx.client.data` look like a\n * user-scoped plane inside a cron, when it is the admin-scoped one.\n */\n setClient(client: RebaseServerClient): void {\n this.client = client;\n }\n\n /**\n * Attach a persistence store for cron logs.\n * When set, execution logs are written to the database after each run,\n * and counters are seeded from the database on start.\n */\n setStore(store: CronStore): void {\n this.store = store;\n }\n\n /**\n * Register a batch of loaded cron jobs.\n *\n * If the scheduler is already started, newly registered jobs are\n * automatically scheduled (so late-registered jobs don't sit idle).\n *\n * Validates the cron schedule on registration — invalid schedules\n * are rejected with a warning and the job is NOT registered.\n */\n registerJobs(loadedJobs: LoadedCronJob[]): void {\n for (const loaded of loadedJobs) {\n // Validate schedule up-front — reject invalid schedules\n const validation = validateCronExpression(loaded.definition.schedule);\n if (!validation.valid) {\n logger.error(`[cron] Rejecting job \"${loaded.id}\": invalid schedule \"${loaded.definition.schedule}\" — ${validation.reason}`);\n // Kept, not just logged. A rejected job is absent from\n // `listJobs()`, so from the Studio panel it is indistinguishable\n // from a file that was never written — and the commonest reason\n // to land here is a 6-field expression copied from a tool that\n // supports seconds, which is a one-character fix nobody could\n // see without boot-log access.\n this.refuse(loaded, validation.reason);\n continue;\n }\n // Rejected, not read as local time: a misspelled zone that fell\n // back silently would fire at the wrong hour on every host and\n // look like a scheduler bug. Kept in `rejected` for the same reason\n // a bad schedule is — otherwise the panel cannot tell it from a\n // file nobody wrote.\n if (loaded.definition.timezone !== undefined && !isValidTimeZone(loaded.definition.timezone)) {\n const reason =\n `unknown timezone \"${loaded.definition.timezone}\" — ` +\n 'use an IANA name such as \"Europe/Madrid\" or \"UTC\"';\n logger.error(`[cron] Rejecting job \"${loaded.id}\": ${reason}.`);\n this.refuse(loaded, reason);\n continue;\n }\n // Same treatment: a timeout that fails every run the moment it\n // starts is a configuration error, not a run-time one.\n const timeoutProblem = invalidTimeoutReason(loaded.definition.timeoutSeconds);\n if (timeoutProblem) {\n logger.error(`[cron] Rejecting job \"${loaded.id}\": ${timeoutProblem}.`);\n this.refuse(loaded, timeoutProblem);\n continue;\n }\n // A re-register that now validates clears the earlier complaint.\n this.rejected.delete(loaded.id);\n\n const existing = this.jobs.get(loaded.id);\n if (existing) {\n logger.warn(`[cron] Duplicate cron job id: \"${loaded.id}\". Overwriting.`);\n this.stopJob(loaded.id);\n }\n\n const enabled = loaded.definition.enabled !== false;\n\n const job: RegisteredJob = {\n id: loaded.id,\n definition: loaded.definition,\n enabled,\n state: enabled ? \"idle\" : \"disabled\",\n totalRuns: 0,\n totalFailures: 0,\n logs: [],\n executing: false\n };\n this.jobs.set(loaded.id, job);\n\n // If the scheduler is already running, auto-schedule new jobs\n if (this.started && this.shouldArm(job)) {\n this.scheduleNext(loaded.id);\n }\n }\n }\n\n /**\n * Start the scheduler — begins ticking all enabled jobs.\n */\n start(): void {\n if (this.started) return;\n this.started = true;\n\n // Seed counters from DB (non-blocking — scheduler starts immediately)\n if (this.store) {\n this.store.fetchJobStats().then((stats) => {\n for (const [jobId, data] of stats) {\n const job = this.jobs.get(jobId);\n if (job) {\n job.totalRuns = data.totalRuns;\n job.totalFailures = data.totalFailures;\n if (data.lastRunAt) {\n job.lastRunAt = new Date(data.lastRunAt);\n }\n }\n }\n }).catch((err) => {\n logger.warn(\"[cron] Failed to seed job stats from database\", { error: err });\n });\n }\n\n for (const [id, job] of this.jobs) {\n if (this.shouldArm(job)) {\n this.scheduleNext(id);\n }\n }\n if (!this.store) {\n logger.warn(\"[cron] No cron store attached — runs are uncoordinated; with multiple app instances every instance will execute every job\");\n }\n this.warnIfScaleToZero();\n\n // Recover slots that elapsed while nothing was ticking. Deliberately\n // not awaited: catch-up reaches the database and runs handlers, and\n // boot must not wait on either. Its own errors are contained inside.\n void this.catchUpMissedSlots();\n\n // Off the boot path too, and then hourly. It cannot take a claim the\n // catch-up is asking about: a job's latest claim is never swept. The\n // first log sweep is also what counts a history that predates the run\n // counters — a backlog it works through in batches, not in the boot.\n if (this.store?.sweepClaims || this.store?.sweepLogs) {\n const sweep = () => {\n void this.sweepClaims();\n void this.sweepLogs();\n };\n sweep();\n this.claimSweepTimer = setInterval(sweep, CLAIM_SWEEP_INTERVAL_MS);\n this.claimSweepTimer.unref?.();\n }\n\n logger.info(`⏰ Cron scheduler started with ${this.jobs.size} job(s)`);\n }\n\n /** Delete slot claims past retention. Failures are logged; the next hour tries again. */\n private async sweepClaims(): Promise<void> {\n const store = this.store;\n if (!store?.sweepClaims || this.sweepingClaims) return;\n this.sweepingClaims = true;\n try {\n await store.sweepClaims();\n } catch (err) {\n logger.warn(\"[cron] The claim retention sweep failed; it runs again within the hour\", { error: err });\n } finally {\n this.sweepingClaims = false;\n }\n }\n\n /**\n * Delete run history past retention, and count the runs not counted yet.\n * Failures are logged; the next hour tries again, from where this one\n * stopped — each batch it finished is committed.\n */\n private async sweepLogs(): Promise<void> {\n const store = this.store;\n if (!store?.sweepLogs || this.sweepingLogs) return;\n this.sweepingLogs = true;\n try {\n const { counted } = await store.sweepLogs();\n if (counted > 0) {\n logger.info(`[cron] Counted ${counted} earlier run(s) into the jobs' run totals`);\n }\n } catch (err) {\n logger.warn(\"[cron] The run history sweep failed; it runs again within the hour\", { error: err });\n } finally {\n this.sweepingLogs = false;\n }\n }\n\n /**\n * Stop the scheduler: clear every timer, then wait for the runs executing\n * right now — for at most `timeoutMs` when given.\n *\n * A run still going when the budget runs out has its `ctx.signal` aborted\n * and ends there, recorded as a failure that says why. Its slot stays\n * claimed, so no other instance re-runs it; a handler that must not be cut\n * short should watch its signal and leave its work resumable.\n *\n * Nothing is scheduled after `stop()`, whether or not the returned promise\n * is awaited: the timers are cleared before it first yields.\n */\n async stop(timeoutMs?: number): Promise<void> {\n this.started = false;\n for (const [id] of this.jobs) {\n this.stopJob(id);\n }\n if (this.claimSweepTimer) {\n clearInterval(this.claimSweepTimer);\n this.claimSweepTimer = undefined;\n }\n if (this.inFlight.size === 0) return;\n\n const settled = Promise.allSettled([...this.inFlight]).then(() => true);\n if (timeoutMs === undefined) {\n await settled;\n return;\n }\n let budget: ReturnType<typeof setTimeout> | undefined;\n const finished = await Promise.race([\n settled,\n new Promise<false>((resolve) => {\n budget = setTimeout(() => resolve(false), timeoutMs);\n if (budget && typeof budget === \"object\" && \"unref\" in budget) {\n budget.unref();\n }\n })\n ]);\n clearTimeout(budget);\n if (finished || this.runControllers.size === 0) return;\n\n logger.warn(`[cron] ${this.runControllers.size} run(s) still executing after ${timeoutMs}ms — aborting them`);\n for (const controller of this.runControllers) {\n controller.abort(new Error(\"The server is shutting down; the run was stopped before it finished\"));\n }\n }\n\n /**\n * List all registered jobs with their current status.\n */\n listJobs(): CronJobStatus[] {\n return [...this.jobs.values()].map((job) => this.toStatus(job));\n }\n\n /** Keep a refused job, so the admin surface can say why it never runs. */\n private refuse(loaded: LoadedCronJob, reason: string): void {\n this.rejected.set(loaded.id, {\n job: {\n id: loaded.id,\n name: loaded.definition.name ?? loaded.id,\n schedule: loaded.definition.schedule,\n reason\n },\n definition: loaded.definition\n });\n }\n\n /**\n * Jobs that loaded but whose schedule the scheduler refused.\n *\n * Kept apart from {@link listJobs} because they are not jobs — nothing will\n * run them — but they must be reachable, or \"my cron is missing\" and \"my\n * cron will never fire\" look identical from the admin panel.\n */\n listRejectedJobs(): RejectedCronJob[] {\n return [...this.rejected.values()].map(refused => refused.job);\n }\n\n /**\n * {@link listRejectedJobs}, those whose definition matches — how a surface\n * finds a refused job by what it is, as {@link jobIdsWhere} does for the\n * registered ones.\n */\n rejectedJobsWhere(predicate: (definition: CronJobDefinition) => boolean): RejectedCronJob[] {\n return [...this.rejected.values()].filter(refused => predicate(refused.definition)).map(refused => refused.job);\n }\n\n /**\n * The ids of the registered jobs whose definition matches — how a surface\n * finds a job by what it is rather than by the file it happens to live in.\n */\n jobIdsWhere(predicate: (definition: CronJobDefinition) => boolean): string[] {\n return [...this.jobs.values()].filter(job => predicate(job.definition)).map(job => job.id);\n }\n\n /**\n * Get a single job status by ID.\n */\n getJob(id: string): CronJobStatus | undefined {\n const job = this.jobs.get(id);\n return job ? this.toStatus(job) : undefined;\n }\n\n /**\n * {@link listJobs}, as the fleet sees it rather than this process alone.\n *\n * Reads the store: every job's enabled state — a pause another replica\n * made — whether another process holds its run lease, and each job's run\n * count and last run from `cron_logs`. The counters in this process's\n * memory count only the runs it made: the `api` role runs nothing and\n * would say 0 runs for a job the worker has run a thousand times, and one\n * of two scheduling replicas never sees the slots the other one claimed.\n *\n * A store that cannot answer leaves this process's own view, with a\n * warning: a listing is not worth failing.\n */\n async fetchJobs(): Promise<CronJobStatus[]> {\n const jobs = [...this.jobs.values()];\n const views = await this.readFleet(jobs);\n return jobs.map(job => this.toStatus(job, views.get(job.id)));\n }\n\n /** {@link getJob}, as {@link fetchJobs} reads it. */\n async fetchJob(id: string): Promise<CronJobStatus | undefined> {\n const job = this.jobs.get(id);\n if (!job) return undefined;\n const views = await this.readFleet([job]);\n return this.toStatus(job, views.get(id));\n }\n\n /**\n * Get log entries for a job.\n */\n getJobLogs(id: string, limit?: number): CronJobLogEntry[] {\n const job = this.jobs.get(id);\n if (!job) return [];\n const logs = [...job.logs].reverse(); // newest first\n return limit ? logs.slice(0, limit) : logs;\n }\n\n /**\n * Get log entries for a job from the database (if store is available).\n * Falls back to in-memory logs if no store is configured, or if the store\n * has none — a run whose row is still being written.\n *\n * Throws when the store cannot read the history. This process's memory is\n * not a stand-in for it: it is empty on the `api` role and after a\n * restart, and holds one replica's runs at most.\n */\n async getJobLogsFromDb(id: string, limit?: number): Promise<CronJobLogEntry[]> {\n if (this.store) {\n const dbLogs = await this.store.fetchLogs(id, limit);\n if (dbLogs.length > 0) return dbLogs;\n }\n // Fallback to in-memory\n return this.getJobLogs(id, limit);\n }\n\n /**\n * Enable or disable a job in this process only. `null` returns it to what\n * its code declares.\n *\n * {@link persistJobEnabled} is what the admin API calls: this alone is\n * forgotten on restart and reaches no other replica.\n */\n setJobEnabled(id: string, enabled: boolean | null): CronJobStatus | undefined {\n const job = this.jobs.get(id);\n if (!job) return undefined;\n\n const effective = enabled ?? this.codeEnabled(job);\n this.applyEnabled(job, effective);\n\n if (effective && this.started) {\n this.scheduleNext(id);\n } else if (!this.shouldArm(job)) {\n this.stopJob(id);\n }\n\n return this.toStatus(job);\n }\n\n /**\n * Enable or disable a job for every process that runs it, and keep it that\n * way across restarts and redeploys: `true` or `false` overrides the code's\n * `enabled`, `null` hands the decision back to it.\n *\n * The override is written to the store first, and only then applied here,\n * so a write that fails throws and changes nothing — a pause that only the\n * replica serving the request would honour is the failure this exists to\n * end. Every scheduler reads the override when a slot comes due, before it\n * claims the slot.\n *\n * Without a store that keeps the state (a driver with no SQL, persistence\n * switched off, a custom store) this is {@link setJobEnabled}: local, and\n * gone on restart.\n */\n async persistJobEnabled(id: string, enabled: boolean | null, updatedBy?: string): Promise<CronJobStatus | undefined> {\n if (!this.jobs.has(id)) return undefined;\n if (this.store?.saveEnabledOverride) {\n await this.store.saveEnabledOverride(id, enabled, updatedBy);\n }\n return this.setJobEnabled(id, enabled);\n }\n\n /**\n * Manually trigger a job execution immediately.\n *\n * Returns `undefined` if the job doesn't exist.\n * If the job is currently executing — here, or on another process that\n * holds its run lease — returns the log entry with a `skipped: true`\n * result rather than running concurrently; {@link isAlreadyExecutingSkip}\n * recognises it.\n */\n async triggerJob(id: string): Promise<CronJobLogEntry | undefined> {\n const job = this.jobs.get(id);\n if (!job) return undefined;\n\n // Concurrency guard — don't run two instances simultaneously\n if (job.executing) {\n logger.warn(`[cron] Skipping manual trigger of \"${id}\" — already executing`);\n return this.recordSkip(job, \"already_executing\", true);\n }\n\n return this.executeJob(job, true);\n }\n\n /**\n * Start a job now and answer as soon as the run has begun, not when it\n * ends — the manual trigger's `?async=1`.\n *\n * The synchronous {@link triggerJob} holds its HTTP request open for the\n * whole run, and a proxy in front of the server (many stop at 60 s) cuts\n * a long job's answer while the run carries on. This resolves once the\n * run lease is held and the handler is about to be called, so a run that\n * cannot start — already executing here or on the lease holder — is still\n * answered as the skip it is. The run itself is tracked like any other:\n * `stop()` waits for it, and its outcome lands in the run history, where\n * the entry carries `manual: true` and the `startedAt` returned here.\n *\n * Returns `undefined` if the job doesn't exist.\n */\n async startJob(id: string): Promise<StartedCronRun | undefined> {\n const job = this.jobs.get(id);\n if (!job) return undefined;\n\n if (job.executing) {\n logger.warn(`[cron] Skipping manual trigger of \"${id}\" — already executing`);\n return { started: false, log: this.recordSkip(job, \"already_executing\", true) };\n }\n\n let onStart!: (startedAt: string) => void;\n const begun = new Promise<string>((resolve) => { onStart = resolve; });\n const run = this.executeJob(job, true, undefined, onStart);\n // Whichever comes first: the handler being called, or the run ending\n // without it (a skip). A run that starts settles `begun` first, since\n // `onStart` is called before the handler is awaited.\n const first = await Promise.race([\n begun.then((startedAt) => ({ startedAt })),\n run.then((log) => ({ log }))\n ]);\n if (\"startedAt\" in first) return { started: true, startedAt: first.startedAt };\n return { started: false, log: first.log };\n }\n\n /**\n * Record a run that did not happen because the previous one had not\n * finished.\n *\n * Written to `cron_logs`, not only to the in-memory ring. An overlap is the\n * signature of a job that has outgrown its schedule — the one thing you want\n * to see in the history rather than infer from a gap in it — and until now\n * the scheduled path left no trace at all beyond a warning in the process\n * log, which is gone by the time anyone asks. The manual path wrote a ring\n * entry that vanished on restart.\n *\n * `success: true` is deliberate: nothing failed. The `result.skipped` flag\n * and the reason are what distinguishes it, and the Studio panel reads them.\n */\n private recordSkip(job: RegisteredJob, reason: string, manual: boolean, runningOn?: string): CronJobLogEntry {\n const now = new Date().toISOString();\n const why = reason === \"already_executing\" ? \"the previous run has not finished\" : reason;\n const logEntry: CronJobLogEntry = {\n jobId: job.id,\n startedAt: now,\n finishedAt: now,\n durationMs: 0,\n success: true,\n result: { skipped: true, reason },\n logs: [`Skipped: ${why}${runningOn ? ` — it is running on ${runningOn}` : \"\"}`],\n manual\n };\n\n job.logs.push(logEntry);\n if (job.logs.length > MAX_LOGS_PER_JOB) job.logs.shift();\n\n // Tracked like a run's own write, so a shutdown does not close the pool\n // under it.\n if (this.store) {\n this.track(this.store.insertLog(logEntry).catch((persistErr) => {\n logger.error(`[cron] Failed to persist skip for \"${job.id}\"`, { error: persistErr });\n }));\n }\n\n return logEntry;\n }\n\n // ─── Internal ────────────────────────────────────────────────────\n\n /** What the job's own definition says, before any override. */\n private codeEnabled(job: RegisteredJob): boolean {\n return job.definition.enabled !== false;\n }\n\n /**\n * Whether the enabled state lives in the store, where every process reads\n * it, rather than in this process alone.\n */\n private sharesEnabledState(): boolean {\n return Boolean(this.store?.fetchJobStates && this.store.saveEnabledOverride);\n }\n\n /**\n * Whether a job gets a timer here. A job paused in this process's memory\n * needs none — nothing but this process can resume it. A job whose state\n * the store keeps does, paused or not: another replica may resume it, and\n * reading the state when its slot comes due is how this one finds out.\n */\n private shouldArm(job: RegisteredJob): boolean {\n return job.enabled || this.sharesEnabledState();\n }\n\n /** Set a job's enabled flag, and its state with it unless a run is in progress. */\n private applyEnabled(job: RegisteredJob, enabled: boolean): void {\n job.enabled = enabled;\n if (job.executing) return;\n if (!enabled) job.state = \"disabled\";\n else if (job.state === \"disabled\") job.state = \"idle\";\n }\n\n /**\n * Whether a job may run now: the store's override when it keeps one, else\n * the code's `enabled`.\n *\n * One query per fire, before the slot is claimed — a paused job does not\n * spend its slot. A store that cannot answer falls back to the code's\n * `enabled`, with a warning: a scheduled run fails open, as it does when\n * the claims table cannot answer, because a broken state table must not\n * silently stop every job.\n */\n private async isEnabledNow(job: RegisteredJob): Promise<boolean> {\n if (!this.store?.fetchJobStates || !this.sharesEnabledState()) return job.enabled;\n try {\n const state = (await this.store.fetchJobStates([job.id])).get(job.id);\n const enabled = state?.enabled ?? this.codeEnabled(job);\n this.applyEnabled(job, enabled);\n return enabled;\n } catch (err) {\n const enabled = this.codeEnabled(job);\n logger.warn(\n `[cron] Could not read the enabled state of \"${job.id}\" — falling back to the code's ` +\n `enabled (${enabled}); a pause made from the admin API is not being honoured`,\n { error: err }\n );\n return enabled;\n }\n }\n\n /**\n * How long a run's lease lasts: the run's timeout and a grace, so that a\n * crashed holder frees the job once its run would have been aborted\n * anyway. No timeout gets {@link MAX_UNBOUNDED_LEASE_SECONDS}.\n */\n private leaseSeconds(job: RegisteredJob): number {\n const timeout = job.definition.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;\n return Number.isFinite(timeout) ? timeout + LEASE_GRACE_SECONDS : MAX_UNBOUNDED_LEASE_SECONDS;\n }\n\n /**\n * Take the job's run lease, the cross-process half of \"already\n * executing\". A store without leases, or one that cannot answer, lets the\n * run go ahead unleased — with a warning in the second case — the same way\n * a claim that cannot answer does.\n */\n private async acquireLease(job: RegisteredJob): Promise<LeaseOutcome> {\n if (!this.store?.tryAcquireRunLease) return { acquired: true };\n const token = `${this.instanceLabel}#${randomUUID().slice(0, 8)}`;\n try {\n const lease = await this.store.tryAcquireRunLease(job.id, token, this.leaseSeconds(job));\n return lease.acquired ? { acquired: true, token } : lease;\n } catch (err) {\n logger.warn(\n `[cron] Could not take the run lease for \"${job.id}\" — running without it, ` +\n \"so another process may run it at the same time\",\n { error: err }\n );\n return { acquired: true };\n }\n }\n\n /** Release a lease this run took. A release that fails leaves it to lapse. */\n private async releaseLease(job: RegisteredJob, token: string): Promise<void> {\n try {\n await this.store?.releaseRunLease?.(job.id, token);\n } catch (err) {\n logger.warn(\n `[cron] Could not release the run lease for \"${job.id}\" — it lapses on its own ` +\n `within ${this.leaseSeconds(job)}s`,\n { error: err }\n );\n }\n }\n\n /**\n * What the store says about `jobs` beyond this process — see\n * {@link fetchJobs}. Applies the enabled state it reads, and returns the\n * rest as a view for {@link toStatus}. Never throws.\n */\n private async readFleet(jobs: RegisteredJob[]): Promise<Map<string, FleetView>> {\n const views = new Map<string, FleetView>();\n const viewOf = (id: string): FleetView => {\n let view = views.get(id);\n if (!view) {\n view = {};\n views.set(id, view);\n }\n return view;\n };\n const store = this.store;\n const ids = jobs.map(job => job.id);\n\n if (store?.fetchJobStates && this.sharesEnabledState() && ids.length > 0) {\n try {\n const states = await store.fetchJobStates(ids);\n for (const job of jobs) {\n const state = states.get(job.id);\n this.applyEnabled(job, state?.enabled ?? this.codeEnabled(job));\n if (state?.runningBy && !job.executing) viewOf(job.id).runningElsewhere = true;\n }\n } catch (err) {\n logger.warn(\"[cron] Could not read the jobs' shared state — listing what this process knows\", { error: err });\n }\n }\n\n // Only where nothing is scheduled: a started scheduler has its own next\n // slots.\n if (!this.started) {\n const now = new Date();\n for (const job of jobs) {\n try {\n viewOf(job.id).nextRunAt = this.listedNextRun(job, now);\n } catch {\n // A schedule with no reachable slot has no next run to show.\n }\n }\n }\n // Wherever this process stands. Its own counters are seeded from the\n // store once, at start, and after that count only its own runs:\n // with two replicas scheduling, a slot the other one claimed never\n // reached them, and the card said \"idle, 0 runs\" over a history that\n // listed the failure.\n if (store?.fetchRunSummaries && ids.length > 0) {\n try {\n const summaries = await store.fetchRunSummaries(ids);\n for (const [id, summary] of summaries) viewOf(id).summary = summary;\n } catch (err) {\n logger.warn(\"[cron] Could not read the jobs' run history — listing what this process knows\", { error: err });\n }\n }\n return views;\n }\n\n /**\n * The next slot of `job` after `now`, for a listing on a process whose\n * scheduler is not started — the `api` role, which Studio's Cron panel\n * polls every 15 seconds.\n *\n * Cached on the job. The key is the job's schedule and zone (a re-register\n * replaces the job, and with it the cache); the entry holds while\n * `computedAt <= now < slot`. That is the same answer, not an\n * approximation: the search found no matching minute between\n * `computedAt`'s next minute and `slot`, so from any later instant before\n * `slot` it lands on `slot` again. A clock that moves back, or reaching the\n * slot, recomputes. The cache holds a pure function of the schedule, so\n * there is nothing user- or role-scoped in it.\n */\n private listedNextRun(job: RegisteredJob, now: Date): Date {\n const { schedule, timezone } = job.definition;\n const nowMs = now.getTime();\n const cached = job.listedSlot;\n if (\n cached\n && cached.schedule === schedule\n && cached.timezone === timezone\n && cached.computedAt <= nowMs\n && nowMs < cached.slot.getTime()\n ) {\n return cached.slot;\n }\n const slot = parseCronExpression(schedule, now, timezone);\n job.listedSlot = { schedule, timezone, computedAt: nowMs, slot };\n return slot;\n }\n\n /**\n * Warn once at start when the process looks like it is running on a\n * platform that freezes or evicts instances between requests, where the\n * in-process timers this scheduler relies on never fire.\n *\n * Advisory only: any failure here is swallowed so a detection bug can\n * never take a production boot down.\n */\n private warnIfScaleToZero(): void {\n try {\n const warning = buildScaleToZeroWarning(\n [...this.jobs.values()].map((job) => ({ id: job.id, enabled: job.enabled }))\n );\n if (warning) {\n logger.warn(warning.message, warning.data);\n }\n } catch {\n // Never let the advisory check affect startup.\n }\n }\n\n /**\n * Schedule the next execution for a job.\n *\n * Safety guarantees:\n * 1. Clears any existing timer first (prevents leaked/duplicate timers)\n * 2. Enforces a minimum delay to prevent tight loops from jitter\n * 3. Unref's the timer so it doesn't prevent process exit\n * 4. Re-checks enabled & started state before executing\n * 5. Concurrency guard prevents overlapping handler executions\n */\n private scheduleNext(id: string): void {\n const job = this.jobs.get(id);\n if (!job || !this.started || !this.shouldArm(job)) return;\n\n // Clear any previously scheduled timer to prevent double-firing\n this.stopJob(id);\n\n try {\n const now = new Date();\n const nextRun = parseCronExpression(job.definition.schedule, now, job.definition.timezone);\n job.nextRunAt = nextRun;\n\n const rawDelay = nextRun.getTime() - now.getTime();\n // Enforce a minimum delay to prevent tight re-execution loops\n // from event loop jitter or near-zero setTimeout drift\n const delay = Math.max(rawDelay, MIN_SCHEDULE_INTERVAL_MS);\n\n // A slot past the 32-bit timer ceiling cannot be armed directly:\n // setTimeout would clamp it to 1ms and fire at once, and since the\n // slot is already claimed by then, every wake re-schedules the same\n // overflowing delay — a permanent hot loop, not a late job. Sleep\n // to the ceiling and re-derive the delay on waking instead; the\n // cron expression stays the source of truth across the hops.\n if (delay > MAX_TIMER_DELAY_MS) {\n const hop = setTimeout(() => {\n if (this.started && this.shouldArm(job)) this.scheduleNext(id);\n }, MAX_TIMER_DELAY_MS);\n if (hop && typeof hop === \"object\" && \"unref\" in hop) {\n hop.unref();\n }\n job.timerId = hop;\n return;\n }\n\n const timer = setTimeout(async () => {\n // Re-check state: the scheduler may have been stopped between\n // when we scheduled and when we fire\n if (!this.started) return;\n\n // A timer can wake before its slot: a delay past the 32-bit\n // ceiling, a clock stepped backwards by NTP, a VM resuming from\n // suspend. Claiming on an early wake is unrecoverable — claims\n // are permanent, so the slot would be burned and the real run\n // silently skipped when it came due. Re-derive from the wall\n // clock and re-arm instead; only the fire that is genuinely due\n // may claim.\n if (Date.now() < nextRun.getTime()) {\n this.scheduleNext(id);\n return;\n }\n\n // Paused or resumed — perhaps by another replica, perhaps\n // before this process started. Asked of the store at every\n // fire, and before the claim, so a paused job leaves its slot\n // unspent.\n if (!(await this.isEnabledNow(job))) {\n if (this.started && this.shouldArm(job)) this.scheduleNext(id);\n return;\n }\n if (!this.started) return;\n\n // Concurrency guard: if somehow we're already executing, skip\n if (job.executing) {\n logger.warn(`[cron] Skipping scheduled run of \"${id}\" — still executing from previous run`);\n // Recorded as well as logged: a job that keeps overlapping\n // has outgrown its schedule, and that is visible in the run\n // history or nowhere.\n this.recordSkip(job, \"already_executing\", false);\n // Re-schedule to try again later\n this.scheduleNext(id);\n return;\n }\n\n // Cross-instance guard: claim the scheduled slot in the store.\n // The slot is the scheduled fire time — deterministic across\n // instances — so exactly one instance wins each (job, slot) pair.\n // A store without tryClaimRun (pre-claims custom implementation)\n // runs uncoordinated; a throwing store fails open — either way\n // this callback must never reject, or the job would silently\n // stop rescheduling.\n if (this.store?.tryClaimRun) {\n let claimed = true;\n try {\n claimed = await this.store.tryClaimRun(id, nextRun.toISOString());\n } catch (err) {\n logger.warn(`[cron] Claim check threw for \"${id}\" — running uncoordinated`, { error: err });\n }\n if (!claimed) {\n logger.info(`[cron] Slot ${nextRun.toISOString()} for \"${id}\" claimed by another instance — skipping`);\n if (this.started && this.shouldArm(job)) {\n this.scheduleNext(id);\n }\n return;\n }\n }\n\n // Takes the run lease: a manual trigger in progress on another\n // process — the api role's, say — turns this slot into a\n // recorded skip rather than a second run beside it.\n await this.executeJob(job, false);\n\n // Schedule the next tick (only if still started + armed)\n if (this.started && this.shouldArm(job)) {\n this.scheduleNext(id);\n }\n }, delay);\n\n // Unref the timer so it doesn't prevent Node.js from exiting\n // during graceful shutdown\n if (timer && typeof timer === \"object\" && \"unref\" in timer) {\n timer.unref();\n }\n\n job.timerId = timer;\n } catch (err: unknown) {\n logger.error(`[cron] Failed to schedule \"${id}\"`, { error: err });\n job.state = \"error\";\n job.lastError = err instanceof Error ? err.message : String(err);\n }\n }\n\n /**\n * Run any slot that elapsed while no instance was holding a timer for it.\n *\n * Only jobs that opted in via `catchUpWindowSeconds` are considered, and\n * only their single most recent missed slot — see the field's docs for why\n * both limits are deliberate.\n *\n * The claim is what makes this safe. In the ordinary case — an instance\n * restarting minutes after a slot ran normally — the most recent slot is\n * already claimed, so this costs one `tryClaimRun` per job per boot and\n * does nothing. A slot is only executed when no instance, past or present,\n * ever claimed it.\n *\n * Never throws: a failure here must not take down a scheduler that is\n * otherwise ticking correctly.\n */\n private async catchUpMissedSlots(): Promise<void> {\n // Every job with a window whose state the store may override, not only\n // the ones enabled here: a job the code disables can be resumed from\n // the admin API, and one it enables can be paused there. Each is asked\n // below, before its claim.\n const candidates = [...this.jobs.values()].filter(\n job => this.shouldArm(job) && (job.definition.catchUpWindowSeconds ?? 0) > 0\n );\n if (candidates.length === 0) return;\n\n // A claims-capable store is the whole safety mechanism. Without one,\n // every boot would look like \"this slot never ran\" and an instance\n // recycled twice an hour would re-run the same hourly job twice an\n // hour. Refusing to catch up is the correct degradation.\n if (!this.store?.tryClaimRun) {\n logger.warn(\n `[cron] Catch-up is configured on ${candidates.length} job(s) but no claims-capable store is attached — skipping. ` +\n \"Without claims a restart cannot tell an unrun slot from one the previous instance already ran.\"\n );\n return;\n }\n\n for (const job of candidates) {\n try {\n if (!this.started || job.executing) continue;\n // The persisted pause outlives the deploy that is catching up.\n if (!(await this.isEnabledNow(job))) continue;\n if (!this.started || job.executing) continue;\n\n // Read per job, not once for the pass: the jobs ahead of this\n // one ran their catch-ups first, and this job's own timer may\n // have fired newer slots meanwhile. Its most recent slot is the\n // one as of now, or a stale one runs after newer ones already did.\n const now = new Date();\n const windowSeconds = job.definition.catchUpWindowSeconds!;\n const from = new Date(now.getTime() - windowSeconds * 1000);\n const slot = findMostRecentSlot(job.definition.schedule, from, now, job.definition.timezone);\n if (!slot) continue;\n\n const slotIso = slot.toISOString();\n\n // Same key the scheduled path claims with, so a slot that fired\n // normally is already taken and this is a no-op.\n let claimed: boolean;\n try {\n claimed = await this.store.tryClaimRun(job.id, slotIso);\n } catch (err) {\n // Fail closed, unlike the scheduled path. A missed slot is\n // a recovery, not an obligation — running it against a\n // store that cannot tell us whether it already ran risks a\n // duplicate on every boot.\n logger.warn(`[cron] Catch-up claim threw for \"${job.id}\" — skipping catch-up`, { error: err });\n continue;\n }\n\n if (!claimed) continue;\n\n const lateBy = Math.round((now.getTime() - slot.getTime()) / 1000);\n logger.info(`[cron] Catching up missed slot ${slotIso} for \"${job.id}\" (${lateBy}s late)`);\n\n await this.executeJob(job, false, `⏰ Catch-up run for missed slot ${slotIso} (${lateBy}s late)`);\n } catch (err) {\n logger.error(`[cron] Catch-up failed for \"${job.id}\"`, { error: err });\n }\n }\n }\n\n /**\n * Stop a single job's timer and clear its next run state.\n */\n private stopJob(id: string): void {\n const job = this.jobs.get(id);\n if (job?.timerId) {\n clearTimeout(job.timerId);\n job.timerId = undefined;\n job.nextRunAt = undefined;\n }\n }\n\n /**\n * Execute a job's handler with full isolation and safety.\n *\n * - Sets a concurrency flag to prevent overlapping runs\n * - Wraps handler in a timeout race\n * - Captures all logs, errors, and results\n * - Persists to store (non-blocking) if available\n * - Always restores state even on catastrophic errors\n */\n private executeJob(\n job: RegisteredJob,\n manual: boolean,\n seedLog?: string,\n onStart?: (startedAt: string) => void\n ): Promise<CronJobLogEntry> {\n // Aborted when the run is over before its handler is: the timeout\n // below, or `stop()` giving up on it. Without it the timeout only\n // stopped the scheduler waiting: the handler's `fetch` kept its socket,\n // so a job whose timeout matches its interval leaked one abandoned\n // request per tick while every run was already marked failed.\n const abort = new AbortController();\n this.runControllers.add(abort);\n const run = this.runJob(job, manual, abort, seedLog, onStart);\n this.track(run);\n return run.finally(() => {\n this.runControllers.delete(abort);\n });\n }\n\n /** Hold `stop()` until `work` settles. */\n private track(work: Promise<unknown>): void {\n this.inFlight.add(work);\n const release = () => { this.inFlight.delete(work); };\n work.then(release, release);\n }\n\n private async runJob(\n job: RegisteredJob,\n manual: boolean,\n abort: AbortController,\n seedLog?: string,\n onStart?: (startedAt: string) => void\n ): Promise<CronJobLogEntry> {\n // Checked here as well as by each caller, because each caller awaits\n // something (a claim, a state read) between its own check and this.\n if (job.executing) {\n return this.recordSkip(job, \"already_executing\", manual);\n }\n // Set executing flag — prevents concurrent runs. Taken before the lease\n // is awaited, so a second trigger on this process cannot slip in.\n job.executing = true;\n\n // The lease is what keeps the other processes off: the worker's\n // scheduled run and the api role's manual trigger never saw each\n // other's in-process flag.\n const lease = await this.acquireLease(job);\n if (!lease.acquired) {\n job.executing = false;\n logger.warn(`[cron] Skipping ${manual ? \"manual trigger\" : \"scheduled run\"} of \"${job.id}\" — it is running on ${lease.holder ?? \"another process\"}`);\n return this.recordSkip(job, \"already_executing\", manual, lease.holder ?? \"another process\");\n }\n\n const startedAt = new Date();\n // A caller-supplied first line, stored with the run's own output. A\n // catch-up uses it to say so in the persisted log, where an operator\n // reading `cron_logs` will actually see it — `manual` is the only other\n // provenance the entry carries, and a catch-up is not manual.\n const capturedLogs = createLogCapture(seedLog || undefined);\n\n const ctx: CronJobContext = {\n jobId: job.id,\n scheduledAt: startedAt,\n signal: abort.signal,\n log: (...args: unknown[]) => {\n const line = args.map((a) =>\n typeof a === \"string\" ? a : JSON.stringify(a)\n ).join(\" \");\n // Capped — see `MAX_CAPTURED_LOG_LINES`. The process log below\n // is not: it gets every line.\n capturedLogs.add(line);\n // Also to the process log. `cron_logs` is behind the admin\n // API, so a line that went only there was missing from the\n // platform's logs, which is where an operator looks when a\n // run goes wrong. The values are redacted on the way: a\n // stringified object reaches the logger as text, and the\n // logger's own pass over the message only removes `Failed\n // query:` spans from it.\n logger.info(\n args.map((a) => typeof a === \"string\" ? a : stringifyForLog(a)).join(\" \"),\n { cron: job.id }\n );\n },\n // `rebase`, and only `rebase`: it matches the singleton import and\n // `defineFunction`'s context. The old `client` alias re-exposed\n // `client.data`, the name `RebaseServerClient` deliberately omits so\n // that the RLS-bypassing plane is spelled `dataAsAdmin` everywhere.\n rebase: this.client!\n };\n\n job.state = \"running\";\n job.lastRunAt = startedAt;\n job.totalRuns++;\n onStart?.(startedAt.toISOString());\n\n let success = true;\n let error: string | undefined;\n let result: unknown;\n\n // The run ends when the handler settles or when `abort` fires,\n // whichever is first — so an abort ends the run even for a handler\n // that ignores its signal, and the run is recorded rather than left\n // \"running\" forever.\n const aborted = new Promise<never>((_, reject) => {\n const end = () => reject(abort.signal.reason);\n if (abort.signal.aborted) end();\n else abort.signal.addEventListener(\"abort\", end, { once: true });\n });\n // Handled here as well as by the race: a handler that throws before\n // the race is entered leaves nothing else listening.\n aborted.catch(() => undefined);\n\n try {\n // Race with timeout. Past the 32-bit timer ceiling a plain\n // setTimeout fires after 1ms, so a month-long timeout would fail\n // every run at once; Infinity is no timeout at all.\n const timeout = (job.definition.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1000;\n const cancelTimeout = Number.isFinite(timeout)\n ? setLongTimeout(\n () => abort.abort(new Error(`Cron job \"${job.id}\" timed out after ${timeout}ms`)),\n timeout\n )\n : undefined;\n\n try {\n result = await Promise.race([Promise.resolve(job.definition.handler(ctx)), aborted]);\n } finally {\n cancelTimeout?.();\n }\n } catch (err: unknown) {\n success = false;\n // Redacted at the point of capture, not just on the way to the log:\n // this string is persisted into `cron_logs` and rendered in the\n // Studio cron panel, and a job that fails on a query would\n // otherwise store `Failed query: <sql>\\nparams: <values>` — the\n // statement and every bound value — in a table, indefinitely.\n error = redactSensitiveText(err instanceof Error ? err.message : String(err));\n job.totalFailures++;\n } finally {\n // The lease before the flag: in between, a trigger on this process\n // would pass the flag and then find its own process's lease held.\n if (lease.token) await this.releaseLease(job, lease.token);\n // Always clear executing flag — even on catastrophic errors\n job.executing = false;\n }\n\n const finishedAt = new Date();\n const durationMs = finishedAt.getTime() - startedAt.getTime();\n\n job.state = success ? (job.enabled ? \"idle\" : \"disabled\") : \"error\";\n job.lastDurationMs = durationMs;\n job.lastError = error;\n\n const logEntry: CronJobLogEntry = {\n jobId: job.id,\n startedAt: startedAt.toISOString(),\n finishedAt: finishedAt.toISOString(),\n durationMs,\n success,\n error,\n result: result !== undefined ? result : undefined,\n logs: capturedLogs.close(),\n manual\n };\n\n // Push to ring buffer\n job.logs.push(logEntry);\n if (job.logs.length > MAX_LOGS_PER_JOB) {\n job.logs.shift();\n }\n\n // Persist to database (non-blocking for the run, but tracked, so a\n // shutdown does not close the pool under the write)\n if (this.store) {\n this.track(this.store.insertLog(logEntry).catch((persistErr) => {\n logger.error(`[cron] Failed to persist log for \"${job.id}\"`, { error: persistErr });\n }));\n }\n\n if (success) {\n logger.info(`✅ [cron] \"${job.id}\" completed in ${durationMs}ms`);\n } else {\n logger.error(`❌ [cron] \"${job.id}\" failed in ${durationMs}ms: ${error}`);\n }\n\n return logEntry;\n }\n\n /**\n * A job's status: this process's own record, overlaid with what `view`\n * read from the store. A paused job shows no next run, even where its\n * timer stays armed to read the state again at that slot.\n */\n private toStatus(job: RegisteredJob, view: FleetView = {}): CronJobStatus {\n const summary = view.summary;\n const nextRunAt = job.enabled ? (job.nextRunAt ?? view.nextRunAt) : undefined;\n let state = job.state;\n if (!job.executing) {\n if (view.runningElsewhere) state = \"running\";\n else if (summary && job.enabled) state = summary.lastSuccess === false ? \"error\" : \"idle\";\n }\n return {\n id: job.id,\n // Same fallback the two `listJobs` paths already applied: `name` is\n // optional and the id is the file's own name.\n name: job.definition.name ?? job.id,\n description: job.definition.description,\n schedule: job.definition.schedule,\n enabled: job.enabled,\n state,\n lastRunAt: summary ? summary.lastRunAt : job.lastRunAt?.toISOString(),\n nextRunAt: nextRunAt?.toISOString(),\n lastDurationMs: summary ? summary.lastDurationMs : job.lastDurationMs,\n lastError: summary ? (summary.lastSuccess === false ? summary.lastError : undefined) : job.lastError,\n totalRuns: summary ? summary.totalRuns : job.totalRuns,\n totalFailures: summary ? summary.totalFailures : job.totalFailures\n };\n }\n}\n"],"mappings":"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"}
@@ -5,9 +5,9 @@ __rebaseCreateRequire(import.meta.url);
5
5
  import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
6
  import "./src-D5Y3N-S4.js";
7
7
  import { t as isSQLAdmin } from "./backend-DTAOsLQc.js";
8
- import { r as logger } from "./logger-Bt32sHFw.js";
8
+ import { a as logger } from "./logger-CW8pmTS6.js";
9
9
  import { n as revokeInternalTableSql, t as REBASE_USER_ROLE } from "./internal-tables-L2t39RdT.js";
10
- import { n as createDdlBootstrapper, r as hasInCauseChain } from "./ddl-bootstrap-BNqi2NZe.js";
10
+ import { n as createDdlBootstrapper, r as hasInCauseChain } from "./ddl-bootstrap-Ck20GSIX.js";
11
11
  //#region src/cron/cron-store.ts
12
12
  var cron_store_exports = /* @__PURE__ */ __exportAll({ createCronStore: () => createCronStore });
13
13
  var TABLE = "rebase.cron_logs";
@@ -574,4 +574,4 @@ function rowToLogEntry(row) {
574
574
  //#endregion
575
575
  export { cron_store_exports as n, createCronStore as t };
576
576
 
577
- //# sourceMappingURL=cron-store-DEpVstUU.js.map
577
+ //# sourceMappingURL=cron-store-DkyfuLOH.js.map