From ef63e6344ffda2740e9dd82e712fa9d222f56949 Mon Sep 17 00:00:00 2001 From: Frederik Wallner Date: Wed, 26 Aug 2026 14:55:21 +0000 Subject: [PATCH] feat(admin): add onScheduleEffect wrapper for scheduled functions Adds an Effect wrapper around firebase-functions/v2/scheduler's onSchedule, following the same conventions as the other trigger wrappers (runtime in options, withSpan tracing, defect logging). Documents the new wrapper in the package README and the create-firebase-function skill. Closes #60 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AEWBZud3svoJnGZZUStRcu --- .../skills/create-firebase-function/SKILL.md | 21 ++++++++- .../references/api_reference.md | 44 ++++++++++++++++++- packages/admin/README.md | 11 +++++ packages/admin/src/lib/functions/functions.ts | 1 + .../admin/src/lib/functions/on-schedule.ts | 38 ++++++++++++++++ 5 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 packages/admin/src/lib/functions/on-schedule.ts diff --git a/.agents/skills/create-firebase-function/SKILL.md b/.agents/skills/create-firebase-function/SKILL.md index 84927a3e..5c27f915 100644 --- a/.agents/skills/create-firebase-function/SKILL.md +++ b/.agents/skills/create-firebase-function/SKILL.md @@ -4,7 +4,7 @@ description: | Create Firebase Cloud Functions using effect-firebase library with type-safe Effect patterns. Use when: (1) Creating callable functions (onCallEffect), (2) Creating HTTP endpoints (onRequestEffect), (3) Creating Firestore triggers (onDocumentCreated/Updated/Deleted/Written), (4) Creating Pub/Sub handlers (onMessagePublishedEffect), - (5) Setting up function runtime, (6) Adding schema validation to functions. + (5) Creating scheduled functions (onScheduleEffect), (6) Setting up function runtime, (7) Adding schema validation to functions. --- # Effect Firebase Functions @@ -240,6 +240,25 @@ export const handleNotification = onMessagePublishedEffect( ); ``` +### Scheduled Function + +```typescript +import { onScheduleEffect } from '@effect-firebase/admin'; + +export const dailyCleanup = onScheduleEffect( + { + runtime, + schedule: 'every 24 hours', + timeZone: 'Europe/Copenhagen', // Optional + }, + (event) => + Effect.gen(function* () { + // event.jobName and event.scheduleTime available + yield* Effect.log(`Cleanup triggered at ${event.scheduleTime}`); + }) +); +``` + ## Best Practices 1. **Single runtime**: Share one runtime across all functions diff --git a/.agents/skills/create-firebase-function/references/api_reference.md b/.agents/skills/create-firebase-function/references/api_reference.md index 1c094b08..8024b1b5 100644 --- a/.agents/skills/create-firebase-function/references/api_reference.md +++ b/.agents/skills/create-firebase-function/references/api_reference.md @@ -7,8 +7,9 @@ 3. [onRequestEffect](#onrequesteffect) 4. [Firestore Triggers](#firestore-triggers) 5. [Pub/Sub](#pubsub) -6. [Context Types](#context-types) -7. [Error Handling](#error-handling) +6. [Scheduler](#scheduler) +7. [Context Types](#context-types) +8. [Error Handling](#error-handling) --- @@ -347,6 +348,45 @@ interface PubSubOptions { --- +## Scheduler + +### onScheduleEffect + +```typescript +function onScheduleEffect( + options: { + runtime: Runtime; + schedule: string; + } & ScheduleOptions, + handler: (event: ScheduledEvent) => Effect.Effect +): ScheduleFunction; +``` + +### ScheduleOptions (from firebase-functions) + +```typescript +interface ScheduleOptions { + schedule: string; // Unix Crontab or AppEngine syntax + timeZone?: string; + retryCount?: number; + maxRetrySeconds?: number; + minBackoffSeconds?: number; + maxBackoffSeconds?: number; + maxDoublings?: number; +} +``` + +### ScheduledEvent + +```typescript +interface ScheduledEvent { + jobName?: string; // Cloud Scheduler job name (undefined when invoked manually) + scheduleTime: string; // RFC3339 UTC schedule time +} +``` + +--- + ## Context Types ### CallableContext diff --git a/packages/admin/README.md b/packages/admin/README.md index e628c890..7be864b1 100644 --- a/packages/admin/README.md +++ b/packages/admin/README.md @@ -104,6 +104,17 @@ export const processEmail = onTaskDispatchedEffect( ); ``` +### Scheduled (`onSchedule`) + +```typescript +import { onScheduleEffect } from '@effect-firebase/admin'; + +export const cleanup = onScheduleEffect( + { runtime, schedule: 'every 24 hours' }, + (event) => Effect.log(`Running cleanup job: ${event.jobName}`), +); +``` + ## Cloud Logging `Admin.layer` automatically replaces the default Effect logger with one that writes structured logs to Cloud Logging: diff --git a/packages/admin/src/lib/functions/functions.ts b/packages/admin/src/lib/functions/functions.ts index 78c6f741..efbec3e7 100644 --- a/packages/admin/src/lib/functions/functions.ts +++ b/packages/admin/src/lib/functions/functions.ts @@ -8,3 +8,4 @@ export * from './on-document-deleted.js'; export * from './on-document-written.js'; export * from './on-message-published.js'; export * from './on-task-dispatched.js'; +export * from './on-schedule.js'; diff --git a/packages/admin/src/lib/functions/on-schedule.ts b/packages/admin/src/lib/functions/on-schedule.ts new file mode 100644 index 00000000..1427171f --- /dev/null +++ b/packages/admin/src/lib/functions/on-schedule.ts @@ -0,0 +1,38 @@ +import { Effect } from 'effect'; +import { + onSchedule, + ScheduledEvent, + ScheduleFunction, + ScheduleOptions, +} from 'firebase-functions/v2/scheduler'; +import { logger } from 'firebase-functions'; +import { run, Runtime } from './run.js'; + +interface ScheduleEffectOptions extends ScheduleOptions { + runtime: Runtime; +} + +/** + * Create a Firebase Functions scheduled trigger that runs an effect on a schedule. + * + * @param options - The options for the scheduled trigger including the schedule. + * @param handler - The handler function that runs the effect. + * @returns The Firebase Functions scheduled trigger. + */ +export function onScheduleEffect( + options: ScheduleEffectOptions, + handler: (event: ScheduledEvent) => Effect.Effect, +): ScheduleFunction { + return onSchedule(options, async (event) => { + const effect = handler(event).pipe(Effect.withSpan('onScheduleEffect')); + + await run(options.runtime, effect as Effect.Effect).catch( + (error) => { + logger.error('Defect in onSchedule', { + inner: error, + stack: error instanceof Error ? error.stack : undefined, + }); + }, + ); + }); +}