import type { RetryOption, RunResult } from "@convex-dev/workpool"; import { BaseChannel } from "async-channel"; import { parse } from "convex-helpers/validators"; import type { FunctionArgs, FunctionReference, FunctionReturnType, FunctionType, FunctionVisibility, } from "convex/server"; import type { Validator } from "convex/values"; import type { EventId, SchedulerOptions, WorkflowId } from "../types.js"; import { safeFunctionName } from "./safeFunctionName.js"; import type { StepRequest } from "./step.js"; export type RunOptions = { /** * The name of the function. By default, if you pass in api.foo.bar.baz, * it will use "foo/bar:baz" as the name. If you pass in a function handle, * it will use the function handle directly. */ name?: string; } & ( | { /** * Run the query or mutation inline within the workflow's transaction, * instead of dispatching it through the work pool. * * This avoids the round-trip overhead of scheduling through the work * pool, but means the function shares the workflow's transaction — * reads and writes are part of the same commit. Avoid using this for * functions that read or write large amounts of data, since they will * count toward the workflow transaction's limits. * * Only applies to queries and mutations. Actions always run via the * work pool. Cannot be combined with `runAfter` or `runAt`. */ inline?: boolean; runAt?: never; runAfter?: never; } | (SchedulerOptions & { inline?: never }) ); export type WorkflowCtx = { /** * The ID of the workflow currently running. */ workflowId: WorkflowId; /** * Run a query with the given name and arguments. * * @param query - The query to run, like `internal.index.exampleQuery`. * @param args - The arguments to the query function. * @param opts - Options for scheduling and naming the query. */ runQuery>( query: Query, ...args: OptionalRestArgs ): Promise>; /** * Run a mutation with the given name and arguments. * * @param mutation - The mutation to run, like `internal.index.exampleMutation`. * @param args - The arguments to the mutation function. * @param opts - Options for scheduling and naming the mutation. */ runMutation< Mutation extends FunctionReference<"mutation", FunctionVisibility>, >( mutation: Mutation, ...args: OptionalRestArgs ): Promise>; /** * Run an action with the given name and arguments. * * @param action - The action to run, like `internal.index.exampleAction`. * @param args - The arguments to the action function. * @param opts - Options for retrying, scheduling and naming the action. */ runAction>( action: Action, ...args: OptionalRestArgs ): Promise>; /** * Run a workflow with the given name and arguments. * * @param workflow - The workflow to run, like `internal.index.exampleWorkflow`. * @param args - The arguments to the workflow function. * @param opts - Options for retrying, scheduling and naming the workflow. */ runWorkflow>( workflow: Workflow, args: FunctionArgs["args"], opts?: RunOptions, ): Promise>; /** * Blocks until a matching event is sent to this workflow. * * If an ID is specified, an event with that ID must already exist and must * not already be "awaited" or "consumed". * * If a name is specified, the first available event is consumed that matches * the name. If there is no available event, it will create one with that name * with status "awaited". * @param event */ awaitEvent( event: ( | { name: Name; id?: EventId } | { name?: Name; id: EventId } ) & { validator?: Validator; }, ): Promise; /** * Suspend execution for the given duration. * * @param duration - The number of milliseconds to sleep. * @param opts - Optionally name the step. Default: "sleep" */ sleep(duration: number, opts?: { name?: string }): Promise; }; export type OptionalRestArgs< Opts, FuncRef extends FunctionReference, > = FuncRef["_args"] extends Record ? [args?: Record, opts?: Opts] : [args: FuncRef["_args"], opts?: Opts]; export function createWorkflowCtx( workflowId: WorkflowId, sender: BaseChannel, ) { return { workflowId, runQuery: async (query, args, opts?) => { return runFunction(sender, "query", query, args, opts); }, runMutation: async (mutation, args, opts?) => { return runFunction(sender, "mutation", mutation, args, opts); }, runAction: async (action, args, opts?) => { return runFunction(sender, "action", action, args, opts); }, runWorkflow: async (workflow, args, opts?) => { const { name, ...schedulerOptions } = opts ?? {}; return run(sender, { name: name ?? safeFunctionName(workflow), target: { kind: "workflow", function: workflow, args, }, retry: undefined, inline: false, schedulerOptions, }); }, sleep: async (duration, opts?) => { await run(sender, { name: opts?.name ?? "sleep", target: { kind: "sleep", args: {}, }, retry: undefined, inline: false, schedulerOptions: { runAfter: duration }, }); }, awaitEvent: async (event) => { const result = await run(sender, { name: event.name ?? event.id ?? "Event", target: { kind: "event", args: { eventId: event.id }, }, retry: undefined, inline: false, schedulerOptions: {}, }); if (event.validator) { return parse(event.validator, result); } return result as any; }, } satisfies WorkflowCtx; } async function runFunction< F extends FunctionReference, >( sender: BaseChannel, functionType: FunctionType, f: F, args: Record | undefined, opts?: RunOptions & RetryOption, ): Promise { const { name, retry, inline, ...schedulerOptions } = opts ?? {}; if ( inline && ("runAt" in schedulerOptions || "runAfter" in schedulerOptions) ) { throw new Error("Cannot combine `inline` with `runAt` or `runAfter`."); } return run(sender, { name: name ?? safeFunctionName(f), target: { kind: "function", functionType, function: f, args: args ?? {}, }, retry, inline: inline ?? false, schedulerOptions, }); } async function run( sender: BaseChannel, request: Omit, ): Promise { let send: Promise; const p = new Promise((resolve) => { send = sender.push({ ...request, resolve, }); }); await send!; const result = await p; switch (result.kind) { case "success": return result.returnValue; case "failed": throw new Error(result.error); case "canceled": throw new Error("Canceled"); default: throw new Error("Unknown result kind: " + (result as any).kind); } }