@rebasepro/server 0.19.0 → 0.19.1-canary.g96b65b4
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.
|
@@ -778,7 +778,7 @@ var CronScheduler = class {
|
|
|
778
778
|
toStatus(job) {
|
|
779
779
|
return {
|
|
780
780
|
id: job.id,
|
|
781
|
-
name: job.definition.name,
|
|
781
|
+
name: job.definition.name ?? job.id,
|
|
782
782
|
description: job.definition.description,
|
|
783
783
|
schedule: job.definition.schedule,
|
|
784
784
|
enabled: job.enabled,
|
|
@@ -795,4 +795,4 @@ var CronScheduler = class {
|
|
|
795
795
|
//#endregion
|
|
796
796
|
export { cron_scheduler_exports as n, validateCronExpression as r, CronScheduler as t };
|
|
797
797
|
|
|
798
|
-
//# sourceMappingURL=cron-scheduler-
|
|
798
|
+
//# sourceMappingURL=cron-scheduler-D47tdB9T.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cron-scheduler-BpPKpl4i.js","names":[],"sources":["../src/cron/scale-to-zero.ts","../src/cron/cron-scheduler.ts"],"sourcesContent":["/**\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\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/** Accepts the usual truthy spellings; anything else (including \"\") is false. */\nfunction isTruthy(value: string | undefined): boolean {\n if (!value) return false;\n const normalised = value.trim().toLowerCase();\n return normalised === \"1\" || normalised === \"true\" || normalised === \"yes\" || normalised === \"on\";\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 (env.VERCEL === \"1\") {\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 (isTruthy(env[CRON_ALWAYS_ON_ENV])) 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} from \"@rebasepro/types\";\nimport type { RebaseServerClient } from \"@rebasepro/types\";\nimport type { LoadedCronJob } from \"./cron-loader\";\nimport type { CronStore } from \"./cron-store\";\nimport { logger, redactSensitiveText } 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 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 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 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 * Whether a minute-precision instant matches every field of the expression.\n *\n * Read in `zone` when one is named, else in the process's own zone — which is\n * whatever the host is set to, UTC in nearly every container. A schedule that\n * names its zone means the same wall-clock hour on every host it runs on.\n */\nfunction matchesCronFields(candidate: Date, fields: CronFields, zone?: string): boolean {\n const wall: WallClock = zone\n ? wallClockIn(candidate, zone)\n : {\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 return fields.months.includes(wall.month)\n && fields.doms.includes(wall.dom)\n && fields.dows.includes(wall.dow)\n && fields.hours.includes(wall.hour)\n && fields.minutes.includes(wall.minute);\n}\n\n/** ~1 year in minutes — the walk bound for both search directions. */\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\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 // Forward-search from `after + 1 minute`\n const candidate = new Date(after);\n candidate.setSeconds(0, 0);\n candidate.setMinutes(candidate.getMinutes() + 1);\n\n for (let i = 0; i < MAX_SLOT_SEARCH_MINUTES; i++) {\n if (matchesCronFields(candidate, fields, timezone)) {\n return candidate;\n }\n candidate.setMinutes(candidate.getMinutes() + 1);\n }\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 a minute at a time from `to`, so the first hit is already\n * the answer — in the common case (a job that ran normally moments ago) that\n * is a handful of iterations, 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 const candidate = new Date(to);\n candidate.setSeconds(0, 0);\n\n for (let i = 0; i < MAX_SLOT_SEARCH_MINUTES && candidate.getTime() >= from.getTime(); i++) {\n if (matchesCronFields(candidate, fields, timezone)) {\n return candidate;\n }\n candidate.setMinutes(candidate.getMinutes() - 1);\n }\n\n return undefined;\n}\n\n// ─── In-memory ring buffer for logs ──────────────────────────────────\n\nconst MAX_LOGS_PER_JOB = 50;\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// ─── 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\n/**\n * A job the scheduler refused, and why.\n *\n * It is not a `CronJobStatus`: it has no state, no next run and no counters,\n * because it was never registered. Reporting it as a job with `state: \"error\"`\n * would be a lie in the other direction — nothing is going to run it.\n */\nexport interface RejectedCronJob {\n id: string;\n name: string;\n schedule: string;\n reason: string;\n}\n\nexport class CronScheduler {\n private jobs = new Map<string, RegisteredJob>();\n private rejected = new Map<string, RejectedCronJob>();\n private started = false;\n private store?: CronStore;\n private client?: RebaseServerClient;\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.rejected.set(loaded.id, {\n id: loaded.id,\n name: loaded.definition.name ?? loaded.id,\n schedule: loaded.definition.schedule,\n reason: validation.reason\n });\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.rejected.set(loaded.id, {\n id: loaded.id,\n name: loaded.definition.name ?? loaded.id,\n schedule: loaded.definition.schedule,\n reason\n });\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 this.jobs.set(loaded.id, {\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\n // If the scheduler is already running, auto-schedule new jobs\n if (this.started && enabled) {\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 (job.enabled) {\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 logger.info(`⏰ Cron scheduler started with ${this.jobs.size} job(s)`);\n }\n\n /**\n * Stop the scheduler and clear all timers.\n *\n * Currently-executing handlers run to completion (they are async),\n * but no further scheduling occurs after stop.\n */\n stop(): void {\n this.started = false;\n for (const [id] of this.jobs) {\n this.stopJob(id);\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 /**\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()];\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 * 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.\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 at runtime.\n */\n setJobEnabled(id: string, enabled: boolean): CronJobStatus | undefined {\n const job = this.jobs.get(id);\n if (!job) return undefined;\n\n job.enabled = enabled;\n\n if (enabled && this.started) {\n job.state = \"idle\";\n this.scheduleNext(id);\n } else if (!enabled) {\n this.stopJob(id);\n job.state = \"disabled\";\n }\n\n return this.toStatus(job);\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, returns the log entry with\n * a `skipped: true` result rather than running concurrently.\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 * 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): CronJobLogEntry {\n const now = new Date().toISOString();\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: ${reason === \"already_executing\" ? \"the previous run has not finished\" : reason}`],\n manual\n };\n\n job.logs.push(logEntry);\n if (job.logs.length > MAX_LOGS_PER_JOB) job.logs.shift();\n\n this.store?.insertLog(logEntry).catch((persistErr) => {\n logger.error(`[cron] Failed to persist skip for \"${job.id}\"`, { error: persistErr });\n });\n\n return logEntry;\n }\n\n // ─── Internal ────────────────────────────────────────────────────\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 || !job.enabled || !this.started) 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 && job.enabled) 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: scheduler may have been stopped or job disabled\n // between when we scheduled and when we fire\n if (!job.enabled || !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 // 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 // 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 && job.enabled) {\n this.scheduleNext(id);\n }\n return;\n }\n }\n\n await this.executeJob(job, false);\n\n // Schedule the next tick (only if still started + enabled)\n if (this.started && job.enabled) {\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 const candidates = [...this.jobs.values()].filter(\n job => job.enabled && (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 const now = new Date();\n\n for (const job of candidates) {\n try {\n if (!this.started || !job.enabled || job.executing) continue;\n\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 async executeJob(\n job: RegisteredJob,\n manual: boolean,\n seedLog?: string\n ): Promise<CronJobLogEntry> {\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: string[] = seedLog ? [seedLog] : [];\n\n // Set executing flag — prevents concurrent runs\n job.executing = true;\n\n // Aborted when the timeout below wins the race. Without it the timeout\n // only stopped the scheduler waiting: the handler's `fetch` kept its\n // socket, so a job whose timeout matches its interval leaked one\n // abandoned request per tick while every run was already marked failed.\n const abort = new AbortController();\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 capturedLogs.push(line);\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\n let success = true;\n let error: string | undefined;\n let result: unknown;\n\n try {\n // Race with timeout\n const timeout = (job.definition.timeoutSeconds ?? 300) * 1000;\n const handlerPromise = Promise.resolve(job.definition.handler(ctx));\n let timeoutHandle: ReturnType<typeof setTimeout>;\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutHandle = setTimeout(\n () => {\n // Abort first, so the handler's in-flight work is\n // cancelled rather than left running past the run it\n // belongs to.\n abort.abort(new Error(`Cron job \"${job.id}\" timed out after ${timeout}ms`));\n reject(new Error(`Cron job \"${job.id}\" timed out after ${timeout}ms`));\n },\n timeout\n );\n });\n\n try {\n result = await Promise.race([handlerPromise, timeoutPromise]);\n } finally {\n clearTimeout(timeoutHandle!);\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 // 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,\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)\n if (this.store) {\n 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 private toStatus(job: RegisteredJob): CronJobStatus {\n return {\n id: job.id,\n name: job.definition.name,\n description: job.definition.description,\n schedule: job.definition.schedule,\n enabled: job.enabled,\n state: job.state,\n lastRunAt: job.lastRunAt?.toISOString(),\n nextRunAt: job.nextRunAt?.toISOString(),\n lastDurationMs: job.lastDurationMs,\n lastError: job.lastError,\n totalRuns: job.totalRuns,\n totalFailures: job.totalFailures\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cron-scheduler-D47tdB9T.js","names":[],"sources":["../src/cron/scale-to-zero.ts","../src/cron/cron-scheduler.ts"],"sourcesContent":["/**\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\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/** Accepts the usual truthy spellings; anything else (including \"\") is false. */\nfunction isTruthy(value: string | undefined): boolean {\n if (!value) return false;\n const normalised = value.trim().toLowerCase();\n return normalised === \"1\" || normalised === \"true\" || normalised === \"yes\" || normalised === \"on\";\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 (env.VERCEL === \"1\") {\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 (isTruthy(env[CRON_ALWAYS_ON_ENV])) 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} from \"@rebasepro/types\";\nimport type { RebaseServerClient } from \"@rebasepro/types\";\nimport type { LoadedCronJob } from \"./cron-loader\";\nimport type { CronStore } from \"./cron-store\";\nimport { logger, redactSensitiveText } 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 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 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 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 * Whether a minute-precision instant matches every field of the expression.\n *\n * Read in `zone` when one is named, else in the process's own zone — which is\n * whatever the host is set to, UTC in nearly every container. A schedule that\n * names its zone means the same wall-clock hour on every host it runs on.\n */\nfunction matchesCronFields(candidate: Date, fields: CronFields, zone?: string): boolean {\n const wall: WallClock = zone\n ? wallClockIn(candidate, zone)\n : {\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 return fields.months.includes(wall.month)\n && fields.doms.includes(wall.dom)\n && fields.dows.includes(wall.dow)\n && fields.hours.includes(wall.hour)\n && fields.minutes.includes(wall.minute);\n}\n\n/** ~1 year in minutes — the walk bound for both search directions. */\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\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 // Forward-search from `after + 1 minute`\n const candidate = new Date(after);\n candidate.setSeconds(0, 0);\n candidate.setMinutes(candidate.getMinutes() + 1);\n\n for (let i = 0; i < MAX_SLOT_SEARCH_MINUTES; i++) {\n if (matchesCronFields(candidate, fields, timezone)) {\n return candidate;\n }\n candidate.setMinutes(candidate.getMinutes() + 1);\n }\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 a minute at a time from `to`, so the first hit is already\n * the answer — in the common case (a job that ran normally moments ago) that\n * is a handful of iterations, 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 const candidate = new Date(to);\n candidate.setSeconds(0, 0);\n\n for (let i = 0; i < MAX_SLOT_SEARCH_MINUTES && candidate.getTime() >= from.getTime(); i++) {\n if (matchesCronFields(candidate, fields, timezone)) {\n return candidate;\n }\n candidate.setMinutes(candidate.getMinutes() - 1);\n }\n\n return undefined;\n}\n\n// ─── In-memory ring buffer for logs ──────────────────────────────────\n\nconst MAX_LOGS_PER_JOB = 50;\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// ─── 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\n/**\n * A job the scheduler refused, and why.\n *\n * It is not a `CronJobStatus`: it has no state, no next run and no counters,\n * because it was never registered. Reporting it as a job with `state: \"error\"`\n * would be a lie in the other direction — nothing is going to run it.\n */\nexport interface RejectedCronJob {\n id: string;\n name: string;\n schedule: string;\n reason: string;\n}\n\nexport class CronScheduler {\n private jobs = new Map<string, RegisteredJob>();\n private rejected = new Map<string, RejectedCronJob>();\n private started = false;\n private store?: CronStore;\n private client?: RebaseServerClient;\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.rejected.set(loaded.id, {\n id: loaded.id,\n name: loaded.definition.name ?? loaded.id,\n schedule: loaded.definition.schedule,\n reason: validation.reason\n });\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.rejected.set(loaded.id, {\n id: loaded.id,\n name: loaded.definition.name ?? loaded.id,\n schedule: loaded.definition.schedule,\n reason\n });\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 this.jobs.set(loaded.id, {\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\n // If the scheduler is already running, auto-schedule new jobs\n if (this.started && enabled) {\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 (job.enabled) {\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 logger.info(`⏰ Cron scheduler started with ${this.jobs.size} job(s)`);\n }\n\n /**\n * Stop the scheduler and clear all timers.\n *\n * Currently-executing handlers run to completion (they are async),\n * but no further scheduling occurs after stop.\n */\n stop(): void {\n this.started = false;\n for (const [id] of this.jobs) {\n this.stopJob(id);\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 /**\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()];\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 * 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.\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 at runtime.\n */\n setJobEnabled(id: string, enabled: boolean): CronJobStatus | undefined {\n const job = this.jobs.get(id);\n if (!job) return undefined;\n\n job.enabled = enabled;\n\n if (enabled && this.started) {\n job.state = \"idle\";\n this.scheduleNext(id);\n } else if (!enabled) {\n this.stopJob(id);\n job.state = \"disabled\";\n }\n\n return this.toStatus(job);\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, returns the log entry with\n * a `skipped: true` result rather than running concurrently.\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 * 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): CronJobLogEntry {\n const now = new Date().toISOString();\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: ${reason === \"already_executing\" ? \"the previous run has not finished\" : reason}`],\n manual\n };\n\n job.logs.push(logEntry);\n if (job.logs.length > MAX_LOGS_PER_JOB) job.logs.shift();\n\n this.store?.insertLog(logEntry).catch((persistErr) => {\n logger.error(`[cron] Failed to persist skip for \"${job.id}\"`, { error: persistErr });\n });\n\n return logEntry;\n }\n\n // ─── Internal ────────────────────────────────────────────────────\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 || !job.enabled || !this.started) 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 && job.enabled) 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: scheduler may have been stopped or job disabled\n // between when we scheduled and when we fire\n if (!job.enabled || !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 // 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 // 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 && job.enabled) {\n this.scheduleNext(id);\n }\n return;\n }\n }\n\n await this.executeJob(job, false);\n\n // Schedule the next tick (only if still started + enabled)\n if (this.started && job.enabled) {\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 const candidates = [...this.jobs.values()].filter(\n job => job.enabled && (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 const now = new Date();\n\n for (const job of candidates) {\n try {\n if (!this.started || !job.enabled || job.executing) continue;\n\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 async executeJob(\n job: RegisteredJob,\n manual: boolean,\n seedLog?: string\n ): Promise<CronJobLogEntry> {\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: string[] = seedLog ? [seedLog] : [];\n\n // Set executing flag — prevents concurrent runs\n job.executing = true;\n\n // Aborted when the timeout below wins the race. Without it the timeout\n // only stopped the scheduler waiting: the handler's `fetch` kept its\n // socket, so a job whose timeout matches its interval leaked one\n // abandoned request per tick while every run was already marked failed.\n const abort = new AbortController();\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 capturedLogs.push(line);\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\n let success = true;\n let error: string | undefined;\n let result: unknown;\n\n try {\n // Race with timeout\n const timeout = (job.definition.timeoutSeconds ?? 300) * 1000;\n const handlerPromise = Promise.resolve(job.definition.handler(ctx));\n let timeoutHandle: ReturnType<typeof setTimeout>;\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutHandle = setTimeout(\n () => {\n // Abort first, so the handler's in-flight work is\n // cancelled rather than left running past the run it\n // belongs to.\n abort.abort(new Error(`Cron job \"${job.id}\" timed out after ${timeout}ms`));\n reject(new Error(`Cron job \"${job.id}\" timed out after ${timeout}ms`));\n },\n timeout\n );\n });\n\n try {\n result = await Promise.race([handlerPromise, timeoutPromise]);\n } finally {\n clearTimeout(timeoutHandle!);\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 // 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,\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)\n if (this.store) {\n 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 private toStatus(job: RegisteredJob): CronJobStatus {\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: job.state,\n lastRunAt: job.lastRunAt?.toISOString(),\n nextRunAt: job.nextRunAt?.toISOString(),\n lastDurationMs: job.lastDurationMs,\n lastError: job.lastError,\n totalRuns: job.totalRuns,\n totalFailures: job.totalFailures\n };\n }\n}\n"],"mappings":"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"}
|
package/dist/index.es.js
CHANGED
|
@@ -26,7 +26,7 @@ import "./proxy-Czngl3p9.js";
|
|
|
26
26
|
import "./request-timeout-DESvlfrS.js";
|
|
27
27
|
import { t as FunctionSelectionError } from "./selection-CRpqKUbt.js";
|
|
28
28
|
import { n as loadCronJobsFromDirectory, r as loadCronJobsWithDiagnostics } from "./cron-loader-DnmIePn_.js";
|
|
29
|
-
import { r as validateCronExpression, t as CronScheduler } from "./cron-scheduler-
|
|
29
|
+
import { r as validateCronExpression, t as CronScheduler } from "./cron-scheduler-D47tdB9T.js";
|
|
30
30
|
import { t as createCronRoutes } from "./cron-routes-D3x2ydMa.js";
|
|
31
31
|
import { t as createCronStore } from "./cron-store-CCQXwgVL.js";
|
|
32
32
|
import { a as parseBackupTimestamp, i as parseBackupDestination, n as createBackupRoutes, o as readBackupBytes, r as listBackupObjects } from "./backup-C8P6Cl3G.js";
|
|
@@ -22279,7 +22279,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
22279
22279
|
let cronScheduler;
|
|
22280
22280
|
if (surfaces.cron || config.cronsDir && ownership.cronScheduler) {
|
|
22281
22281
|
const { loadCronJobsWithDiagnostics } = await import("./cron-loader-DnmIePn_.js").then((n) => n.t);
|
|
22282
|
-
const { CronScheduler } = await import("./cron-scheduler-
|
|
22282
|
+
const { CronScheduler } = await import("./cron-scheduler-D47tdB9T.js").then((n) => n.n);
|
|
22283
22283
|
const { createCronRoutes } = await import("./cron-routes-D3x2ydMa.js").then((n) => n.n);
|
|
22284
22284
|
const { createCronStore } = await import("./cron-store-CCQXwgVL.js").then((n) => n.n);
|
|
22285
22285
|
const { jobs: loadedCronJobs, problems: cronProblems } = config.cronsDir ? await loadCronJobsWithDiagnostics(config.cronsDir) : {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.1-canary.g96b65b4",
|
|
4
4
|
"description": "Database-Agnostic Backend Core for Rebase",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rebase",
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"jsonwebtoken": "^9.0.3",
|
|
55
55
|
"ws": "^8.21.1",
|
|
56
56
|
"zod": "^4.4.3",
|
|
57
|
-
"@rebasepro/
|
|
58
|
-
"@rebasepro/
|
|
59
|
-
"@rebasepro/
|
|
60
|
-
"@rebasepro/
|
|
57
|
+
"@rebasepro/client": "0.19.1-canary.g96b65b4",
|
|
58
|
+
"@rebasepro/common": "0.19.1-canary.g96b65b4",
|
|
59
|
+
"@rebasepro/utils": "0.19.1-canary.g96b65b4",
|
|
60
|
+
"@rebasepro/types": "0.19.1-canary.g96b65b4"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@jest/globals": "^30.4.1",
|