/** * Schedule worker: drives a program's IR schedules on a tick loop, firing the * ones that are due and sweeping expired approvals. This is the self-contained * runner for hosts that do NOT get cron for free (Vercel invokes the generated * cron routes directly; everyone else runs this). * * Deterministic and testable: the clock and the timer are injectable, so tests * never sleep. Cron matching is UTC and pure (see runtime-schedule.isCronDue); * a same-minute dedupe means each cron schedule fires at most once per matching * minute even though the loop ticks several times a minute. * * Spec: docs/spec/semantics.md § "Scheduled Commands". */ import type { CommandResult, ApprovalRequestState } from './runtime-engine'; import type { IRSchedule, IREntity } from './ir'; /** * The minimal runtime surface the worker depends on. `RuntimeEngine` satisfies * it structurally, so production passes a real engine and tests pass a * lightweight fake without constructing one. */ export interface ScheduleRuntime { getSchedules(): IRSchedule[]; getEntities(): IREntity[]; runSchedule(scheduleName: string): Promise; expireApprovals(now?: number): ApprovalRequestState[] | Promise; } /** Opaque timer handle — whatever the injected timer factory returns. */ export type TimerHandle = unknown; /** Passed to `onError` so a caller can log which phase failed. */ export interface ScheduleWorkerErrorContext { phase: 'runSchedule' | 'expireApprovals'; /** Present for the `runSchedule` phase. */ scheduleName?: string; } export interface ScheduleWorkerOptions { /** * Tick interval in ms. Default 30_000 (30s). Must stay below 60_000 so every * matching minute is sampled; the same-minute dedupe keeps firing to once per * minute regardless. */ intervalMs?: number; /** Clock. Default `Date.now`. Read once per tick. */ now?: () => number; /** Timer factory. Default `setInterval`. Injectable so tests drive ticks by hand. */ setTimer?: (callback: () => void, ms: number) => TimerHandle; /** Timer disposer. Default `clearInterval`. */ clearTimer?: (handle: TimerHandle) => void; /** When aborted, the worker stops (equivalent to calling `stop()`). */ signal?: AbortSignal; /** Called for any error thrown while evaluating/running a schedule or expiring approvals. */ onError?: (error: unknown, context: ScheduleWorkerErrorContext) => void; /** Called after a schedule fires, with its result. Observability only. */ onRun?: (scheduleName: string, result: CommandResult) => void; } export interface ScheduleWorkerHandle { /** Stop the loop and dispose the timer. Idempotent. */ stop(): void; /** * Run a single tick immediately. The internal timer calls this; hosts and * tests may await it directly to drive the worker with their own clock. */ tick(): Promise; } /** * Run a single scheduling pass immediately: fire the cron schedules due at * `now` (UTC, to the minute) and sweep expired approvals. Interval/every * schedules are inherently stateful; with no persistent worker their baseline * is `now`, so they do not fire from a lone pass — this is aimed at * platform-triggered cron-route hosting where the platform supplies the cadence. * Use {@link startScheduleWorker} for a self-contained loop that also drives * interval/every schedules. */ export declare function runSchedulesOnce(runtime: ScheduleRuntime, options?: Pick): Promise; /** * Start a long-running schedule worker. Each tick fires every due schedule * (cron matched in UTC with once-per-minute dedupe; interval/every by elapsed * time since last run, first fire one interval after start) and sweeps expired * approvals when the IR declares any. A throwing schedule is reported to * `onError` and never kills the loop. Returns a handle to stop it. * * At-least/at-most-once caveat: within a running process a cron schedule fires * exactly once per matching minute; across a restart it may re-fire in the * minute of the restart, and a schedule whose only matching minute coincides * with the worker's startup partial minute may be missed. */ export declare function startScheduleWorker(runtime: ScheduleRuntime, options?: ScheduleWorkerOptions): ScheduleWorkerHandle; //# sourceMappingURL=schedule-worker.d.ts.map