| 1 | import type * as Effect from "effect/Effect"; |
| 2 | import * as Binding from "../../Binding.ts"; |
| 3 | import type { RetryConfig } from "./Job.ts"; |
| 4 | |
| 5 | /** One Cloud Scheduler invocation. */ |
| 6 | export 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 | |
| 15 | export 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 | |
| 46 | export type ScheduledEventHandler<Req> = ( |
| 47 | event: ScheduledEvent, |
| 48 | ) => Effect.Effect<void, never, Req>; |
| 49 | |
| 50 | export 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 | */ |
| 98 | export interface ScheduleEventSource extends Binding.Service< |
| 99 | ScheduleEventSource, |
| 100 | "GCP.CloudScheduler.ScheduleEventSource", |
| 101 | ScheduleEventSourceService |
| 102 | > {} |
| 103 | |
| 104 | export 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 | */ |
| 113 | export 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)); |