import { WorkflowEntrypoint as CloudflareWorkflowEntrypoint, type WorkflowEvent as CloudflareWorkflowEvent, type WorkflowSleepDuration, type WorkflowStep as CloudflareWorkflowStep, type WorkflowStepConfig, type WorkflowStepContext as CloudflareWorkflowStepContext, type WorkflowStepEvent, type WorkflowTimeoutDuration, } from "cloudflare:workers"; import { ConfigProvider, Context, Effect, Layer, ManagedRuntime, type Scope } from "effect"; import { WorkerConfig, WorkerEnvironment, type WorkerEnv } from "./Environment"; import { ExecutionContext, WorkerContext } from "./Worker"; import * as WorkflowDefinition from "./WorkflowDefinition"; import * as Entrypoint from "./internal/Entrypoint"; import { fromExecutionContext, type RunWaitUntilEffect } from "./internal/WorkerContext"; export interface WorkflowEventService { readonly raw: CloudflareWorkflowEvent; readonly payload: Payload; readonly timestamp: Date; readonly instanceId: string; readonly workflowName: string; } export class WorkflowEvent extends Context.Service()( "effect-cf/WorkflowEvent", ) {} type RunWorkflowStepEffect = (effect: Effect.Effect) => Promise; export interface WorkflowStepService { readonly raw: CloudflareWorkflowStep; do( name: string, effect: Effect.Effect, config?: WorkflowStepConfig, ): Effect.Effect>; readonly sleep: (name: string, duration: WorkflowSleepDuration) => Effect.Effect; readonly sleepUntil: (name: string, timestamp: Date | number) => Effect.Effect; readonly waitForEvent: ( name: string, options: { readonly type: string; readonly timeout?: WorkflowTimeoutDuration | number; }, ) => Effect.Effect, unknown>; } export class WorkflowStep extends Context.Service()( "effect-cf/WorkflowStep", ) {} export class WorkflowStepContext extends Context.Service< WorkflowStepContext, CloudflareWorkflowStepContext >()("effect-cf/WorkflowStepContext") {} const fromWorkflowEvent = ( event: CloudflareWorkflowEvent, ): WorkflowEventService => ({ raw: event as CloudflareWorkflowEvent, payload: event.payload, timestamp: event.timestamp, instanceId: event.instanceId, workflowName: event.workflowName, }); const fromWorkflowStep = ( step: CloudflareWorkflowStep, runPromise: RunWorkflowStepEffect, ): WorkflowStepService => ({ raw: step, do: (name: string, effect: Effect.Effect, config?: WorkflowStepConfig) => Effect.context>().pipe( Effect.flatMap((context) => Effect.tryPromise({ try: () => { const run = (stepContext: CloudflareWorkflowStepContext) => runPromise( Effect.scoped( Effect.provideService( Effect.provideContext( effect as Effect.Effect>, context, ), WorkflowStepContext, stepContext, ), ), ); const rawStep = step as { do( name: string, callback: (context: CloudflareWorkflowStepContext) => Promise, ): Promise; do( name: string, config: WorkflowStepConfig, callback: (context: CloudflareWorkflowStepContext) => Promise, ): Promise; }; return config === undefined ? rawStep.do(name, run) : rawStep.do(name, config, run); }, catch: (cause) => cause, }), ), ) as Effect.Effect>, sleep: (name, duration) => Effect.tryPromise({ try: () => step.sleep(name, duration), catch: (cause) => cause, }), sleepUntil: (name, timestamp) => Effect.tryPromise({ try: () => step.sleepUntil(name, timestamp), catch: (cause) => cause, }), waitForEvent: ( name: string, options: { readonly type: string; readonly timeout?: WorkflowTimeoutDuration | number; }, ) => Effect.tryPromise({ try: () => step.waitForEvent(name, options) as Promise>, catch: (cause) => cause, }), }); type RuntimeContext = ExecutionContext | WorkerContext | WorkerEnvironment | ROut; export type WorkflowRunContext = | RuntimeContext | WorkflowEvent | WorkflowStep | Scope.Scope; export type WorkflowHandler = ( payload: Payload, ) => Effect.Effect>; export interface WorkflowOptions { readonly run: WorkflowHandler; } export type WorkflowClass = new ( ctx: globalThis.ExecutionContext, env: WorkerEnv, ) => CloudflareWorkflowEntrypoint & { run( event: Readonly>, step: CloudflareWorkflowStep, ): Promise; }; export const make = ( layer: Layer.Layer, options: WorkflowOptions, ): WorkflowClass => { class EffectWorkflow extends CloudflareWorkflowEntrypoint { readonly runtime: ManagedRuntime.ManagedRuntime, LayerError>; constructor(ctx: globalThis.ExecutionContext, env: WorkerEnv) { super(ctx, env); let runWaitUntilEffect: RunWaitUntilEffect = () => Promise.reject(new Error("WorkerContext runtime is not initialized")); const services = Layer.mergeAll( Layer.succeed(ExecutionContext, ctx), ConfigProvider.layer(Effect.succeed(WorkerConfig.providerFromEnv(env))), Layer.succeed( WorkerContext, fromExecutionContext(ctx, (effect) => runWaitUntilEffect(effect)), ), Layer.succeed(WorkerEnvironment, env), ) as Layer.Layer; const runtimeLayer = Entrypoint.provideEntrypointServices< ROut, LayerError, ExecutionContext | WorkerContext | WorkerEnvironment >(layer, services); this.runtime = ManagedRuntime.make(runtimeLayer); runWaitUntilEffect = (effect: Effect.Effect) => this.runtime.runPromiseExit(effect as Effect.Effect>); } run( event: Readonly>, step: CloudflareWorkflowStep, ): Promise { const workflowServices = Layer.mergeAll( Layer.succeed(WorkflowEvent, fromWorkflowEvent(event)), Layer.succeed( WorkflowStep, fromWorkflowStep( step, (effect) => this.runtime.runPromise( effect as Effect.Effect>, ) as never, ), ), ); return this.runtime.runPromise( Effect.scoped( options.run(event.payload).pipe(Effect.provide(workflowServices)), ) as Effect.Effect>, ); } } return Entrypoint.assumeEntrypointClass>(EffectWorkflow); }; export const step = ( name: string, effect: Effect.Effect, config?: WorkflowStepConfig, ): Effect.Effect> => Effect.flatMap(WorkflowStep, (workflowStep) => workflowStep.do(name, effect, config), ) as Effect.Effect>; export const sleep = ( name: string, duration: WorkflowSleepDuration, ): Effect.Effect => Effect.flatMap(WorkflowStep, (workflowStep) => workflowStep.sleep(name, duration)); export const sleepUntil = ( name: string, timestamp: Date | number, ): Effect.Effect => Effect.flatMap(WorkflowStep, (workflowStep) => workflowStep.sleepUntil(name, timestamp)); export const waitForEvent = ( name: string, options: { readonly type: string; readonly timeout?: WorkflowTimeoutDuration | number; }, ): Effect.Effect, unknown, WorkflowStep> => Effect.flatMap(WorkflowStep, (workflowStep) => workflowStep.waitForEvent(name, options)); export type Definition< Id extends string = string, Payload extends WorkflowDefinition.Definition.Any["payload"] = WorkflowDefinition.Definition.Any["payload"], Result extends WorkflowDefinition.Definition.Any["result"] = WorkflowDefinition.Definition.Any["result"], > = WorkflowDefinition.Definition; export namespace Definition { export type Any = WorkflowDefinition.Definition.Any; } export type LayerOptions = WorkflowDefinition.LayerOptions; export type TagClass< Self, Id extends string, Payload extends WorkflowDefinition.Definition.Any["payload"], Result extends WorkflowDefinition.Definition.Any["result"], > = WorkflowDefinition.TagClass; export const Tag: () => < Id extends string, Payload extends WorkflowDefinition.Definition.Any["payload"], Result extends WorkflowDefinition.Definition.Any["result"], >( id: Id, definition: { readonly payload: Payload; readonly result: Result; }, ) => TagClass = WorkflowDefinition.Tag; export const implement = WorkflowDefinition.implement; export type Handler< ROut, Self extends WorkflowDefinition.Definition.Any, > = WorkflowDefinition.Handler; export type Options< ROut, Self extends WorkflowDefinition.Definition.Any, > = WorkflowDefinition.Options;