Spaces:
Running
Running
| import type { CronJobCreate, CronJobPatch } from "../types.js"; | |
| import type { CronServiceState } from "./state.js"; | |
| import { | |
| applyJobPatch, | |
| computeJobNextRunAtMs, | |
| createJob, | |
| findJobOrThrow, | |
| isJobDue, | |
| nextWakeAtMs, | |
| recomputeNextRuns, | |
| } from "./jobs.js"; | |
| import { locked } from "./locked.js"; | |
| import { ensureLoaded, persist, warnIfDisabled } from "./store.js"; | |
| import { armTimer, emit, executeJob, stopTimer, wake } from "./timer.js"; | |
| export async function start(state: CronServiceState) { | |
| await locked(state, async () => { | |
| if (!state.deps.cronEnabled) { | |
| state.deps.log.info({ enabled: false }, "cron: disabled"); | |
| return; | |
| } | |
| await ensureLoaded(state); | |
| recomputeNextRuns(state); | |
| await persist(state); | |
| armTimer(state); | |
| state.deps.log.info( | |
| { | |
| enabled: true, | |
| jobs: state.store?.jobs.length ?? 0, | |
| nextWakeAtMs: nextWakeAtMs(state) ?? null, | |
| }, | |
| "cron: started", | |
| ); | |
| }); | |
| } | |
| export function stop(state: CronServiceState) { | |
| stopTimer(state); | |
| } | |
| export async function status(state: CronServiceState) { | |
| return await locked(state, async () => { | |
| await ensureLoaded(state); | |
| return { | |
| enabled: state.deps.cronEnabled, | |
| storePath: state.deps.storePath, | |
| jobs: state.store?.jobs.length ?? 0, | |
| nextWakeAtMs: state.deps.cronEnabled ? (nextWakeAtMs(state) ?? null) : null, | |
| }; | |
| }); | |
| } | |
| export async function list(state: CronServiceState, opts?: { includeDisabled?: boolean }) { | |
| return await locked(state, async () => { | |
| await ensureLoaded(state); | |
| const includeDisabled = opts?.includeDisabled === true; | |
| const jobs = (state.store?.jobs ?? []).filter((j) => includeDisabled || j.enabled); | |
| return jobs.toSorted((a, b) => (a.state.nextRunAtMs ?? 0) - (b.state.nextRunAtMs ?? 0)); | |
| }); | |
| } | |
| export async function add(state: CronServiceState, input: CronJobCreate) { | |
| return await locked(state, async () => { | |
| warnIfDisabled(state, "add"); | |
| await ensureLoaded(state); | |
| const job = createJob(state, input); | |
| state.store?.jobs.push(job); | |
| await persist(state); | |
| armTimer(state); | |
| emit(state, { | |
| jobId: job.id, | |
| action: "added", | |
| nextRunAtMs: job.state.nextRunAtMs, | |
| }); | |
| return job; | |
| }); | |
| } | |
| export async function update(state: CronServiceState, id: string, patch: CronJobPatch) { | |
| return await locked(state, async () => { | |
| warnIfDisabled(state, "update"); | |
| await ensureLoaded(state); | |
| const job = findJobOrThrow(state, id); | |
| const now = state.deps.nowMs(); | |
| applyJobPatch(job, patch); | |
| job.updatedAtMs = now; | |
| if (job.enabled) { | |
| job.state.nextRunAtMs = computeJobNextRunAtMs(job, now); | |
| } else { | |
| job.state.nextRunAtMs = undefined; | |
| job.state.runningAtMs = undefined; | |
| } | |
| await persist(state); | |
| armTimer(state); | |
| emit(state, { | |
| jobId: id, | |
| action: "updated", | |
| nextRunAtMs: job.state.nextRunAtMs, | |
| }); | |
| return job; | |
| }); | |
| } | |
| export async function remove(state: CronServiceState, id: string) { | |
| return await locked(state, async () => { | |
| warnIfDisabled(state, "remove"); | |
| await ensureLoaded(state); | |
| const before = state.store?.jobs.length ?? 0; | |
| if (!state.store) { | |
| return { ok: false, removed: false } as const; | |
| } | |
| state.store.jobs = state.store.jobs.filter((j) => j.id !== id); | |
| const removed = (state.store.jobs.length ?? 0) !== before; | |
| await persist(state); | |
| armTimer(state); | |
| if (removed) { | |
| emit(state, { jobId: id, action: "removed" }); | |
| } | |
| return { ok: true, removed } as const; | |
| }); | |
| } | |
| export async function run(state: CronServiceState, id: string, mode?: "due" | "force") { | |
| return await locked(state, async () => { | |
| warnIfDisabled(state, "run"); | |
| await ensureLoaded(state); | |
| const job = findJobOrThrow(state, id); | |
| const now = state.deps.nowMs(); | |
| const due = isJobDue(job, now, { forced: mode === "force" }); | |
| if (!due) { | |
| return { ok: true, ran: false, reason: "not-due" as const }; | |
| } | |
| await executeJob(state, job, now, { forced: mode === "force" }); | |
| await persist(state); | |
| armTimer(state); | |
| return { ok: true, ran: true } as const; | |
| }); | |
| } | |
| export function wakeNow( | |
| state: CronServiceState, | |
| opts: { mode: "now" | "next-heartbeat"; text: string }, | |
| ) { | |
| return wake(state, opts); | |
| } | |