1import type * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import type { RetryConfig } from "./Job.ts";
4
5/** One Cloud Scheduler invocation. */
6export interface ScheduledEvent {
7 /** RFC3339 time the run was scheduled for (`X-CloudScheduler-ScheduleTime`). */
8 scheduleTime: string;
9 /** Short id of the job that fired (`X-CloudScheduler-JobName`). */
10 jobName: string;
11 /** The job's configured request body, if any. */
12 body: string | undefined;
13}
14
15export interface ScheduleEventSourceProps {
16 /**
17 * Cron (`"0 * * * *"`) or english-like (`"every 5 minutes"`) schedule.
18 */
19 schedule: string;
20 /**
21 * tz-database time zone used to interpret `schedule`.
22 * @default "Etc/UTC"
23 */
24 timeZone?: string;
25 /** UTF-8 body sent with every invocation. */
26 body?: string;
27 /**
28 * Cloud Scheduler location of the backing job.
29 * @default the stack's GCP region (`GCP.Region`, profile region, `us-central1`)
30 */
31 location?: string;
32 /**
33 * Retry policy for failed invocations. The default retries a refused or
34 * failed delivery (e.g. while a new invoker grant propagates) instead
35 * of dropping that run.
36 * @default { retryCount: 5, minBackoffDuration: "10s", maxBackoffDuration: "120s" }
37 */
38 retryConfig?: RetryConfig;
39 /**
40 * Delivery path on the host. Defaults to a deterministic per-schedule
41 * path under `/__alchemy/scheduler/`.
42 */
43 path?: string;
44}
45
46export type ScheduledEventHandler<Req> = (
47 event: ScheduledEvent,
48) => Effect.Effect<void, never, Req>;
49
50export type ScheduleEventSourceService = <Req = never>(
51 id: string,
52 props: ScheduleEventSourceProps,
53 process: ScheduledEventHandler<Req>,
54) => Effect.Effect<void, never, never>;
55
56/**
57 * Event source that invokes the hosting compute on a Cloud Scheduler
58 * schedule — the "cron handler" DX.
59 *
60 * The HTTP-host implementation is `GCP.Run.ScheduleEventSource`
61 * (`GCP.Function` / `GCP.Run.Service`, `GCP.CloudFunctions.Function`): it
62 * creates a Cloud Scheduler job that `POST`s to the host with an OIDC
63 * token for the host's runtime service account, grants that account
64 * `roles/run.invoker` on the host, and verifies the token on every
65 * invocation. A failed handler answers 500 and the job's retry policy
66 * applies.
67 *
68 * Consume it through {@link consumeSchedule}.
69 *
70 * ### Consuming a Schedule
71 * **Example:** Nightly cleanup on a Cloud Run service
72 * ```typescript
73 * export class Api extends GCP.Function<Api>()(
74 * "Api",
75 * { main: import.meta.url },
76 * Effect.gen(function* () {
77 * yield* GCP.CloudScheduler.consumeSchedule(
78 * "NightlyCleanup",
79 * { schedule: "0 3 * * *", timeZone: "America/New_York" },
80 * (event) => Effect.log(`cleanup scheduled for ${event.scheduleTime}`),
81 * );
82 * }).pipe(Effect.provide(GCP.Run.ScheduleEventSource)),
83 * ) {}
84 * ```
85 *
86 * **Example:** Pass a fixed payload
87 * ```typescript
88 * yield* GCP.CloudScheduler.consumeSchedule(
89 * "Digest",
90 * { schedule: "every 1 hours", body: JSON.stringify({ kind: "digest" }) },
91 * (event) => sendDigest(JSON.parse(event.body ?? "{}")),
92 * );
93 * ```
94 *
95 * @binding
96 * @category CloudScheduler
97 */
98export interface ScheduleEventSource extends Binding.Service<
99 ScheduleEventSource,
100 "GCP.CloudScheduler.ScheduleEventSource",
101 ScheduleEventSourceService
102> {}
103
104export const ScheduleEventSource = Binding.Service<ScheduleEventSource>(
105 "GCP.CloudScheduler.ScheduleEventSource",
106);
107
108/**
109 * Run an Effect handler on a Cloud Scheduler schedule. `id` names the
110 * backing job and its delivery route; keep it stable. See
111 * {@link ScheduleEventSource} for the host implementations.
112 */
113export const consumeSchedule = <Req = never>(
114 id: string,
115 props: ScheduleEventSourceProps,
116 process: ScheduledEventHandler<Req>,
117): Effect.Effect<void, never, ScheduleEventSource> =>
118 ScheduleEventSource.use((source) => source(id, props, process));