@convex-dev/workflow
Version:
Convex component for durably executing workflows.
257 lines (241 loc) • 7.72 kB
text/typescript
import {
createFunctionHandle,
FunctionArgs,
FunctionReference,
FunctionReturnType,
FunctionVisibility,
GenericDataModel,
GenericMutationCtx,
GenericQueryCtx,
getFunctionName,
RegisteredMutation,
ReturnValueForOptionalValidator,
} from "convex/server";
import { ObjectType, PropertyValidators, Validator } from "convex/values";
import { api } from "../component/_generated/api.js";
import { OnCompleteArgs, OpaqueIds, UseApi, WorkflowId } from "../types.js";
import { workflowMutation } from "./workflowMutation.js";
import {
NameOption,
RetryOption,
SchedulerOptions,
WorkpoolOptions,
WorkpoolRetryOptions,
} from "@convex-dev/workpool";
export { vWorkflowId } from "../types.js";
import { Step } from "../component/schema.js";
export type { WorkflowId };
export type CallbackOptions = {
/**
* A mutation to run after the function succeeds, fails, or is canceled.
* The context type is for your use, feel free to provide a validator for it.
* e.g.
* ```ts
* export const completion = internalMutation({
* args: {
* workId: workIdValidator,
* context: v.any(),
* result: resultValidator,
* },
* handler: async (ctx, args) => {
* console.log(args.result, "Got Context back -> ", args.context, Date.now() - args.context);
* },
* });
* ```
*/
onComplete?: FunctionReference<
"mutation",
FunctionVisibility,
OnCompleteArgs
> | null;
/**
* A context object to pass to the `onComplete` mutation.
* Useful for passing data from the enqueue site to the onComplete site.
*/
context?: unknown;
};
export type WorkflowStep = {
/**
* The ID of the workflow currently running.
*/
workflowId: string;
/**
* 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 extends FunctionReference<"query", any>>(
query: Query,
args: FunctionArgs<Query>,
opts?: NameOption & SchedulerOptions,
): Promise<FunctionReturnType<Query>>;
/**
* 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", any>>(
mutation: Mutation,
args: FunctionArgs<Mutation>,
opts?: NameOption & SchedulerOptions,
): Promise<FunctionReturnType<Mutation>>;
/**
* 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 extends FunctionReference<"action", any>>(
action: Action,
args: FunctionArgs<Action>,
opts?: NameOption & SchedulerOptions & RetryOption,
): Promise<FunctionReturnType<Action>>;
};
export type WorkflowDefinition<
ArgsValidator extends PropertyValidators,
ReturnsValidator extends Validator<any, "required", any> | void,
ReturnValue extends ReturnValueForOptionalValidator<ReturnsValidator> = any,
> = {
args?: ArgsValidator;
handler: (
step: WorkflowStep,
args: ObjectType<ArgsValidator>,
) => Promise<ReturnValue>;
returns?: ReturnsValidator;
workpoolOptions?: WorkpoolRetryOptions;
};
export type WorkflowStatus =
| { type: "inProgress"; running: OpaqueIds<Step>[] }
| { type: "completed" }
| { type: "canceled" }
| { type: "failed"; error: string };
export class WorkflowManager {
constructor(
private component: UseApi<typeof api>,
public options?: {
workpoolOptions: WorkpoolOptions;
},
) {}
/**
* Define a new workflow.
*
* @param workflow - The workflow definition.
* @returns The workflow mutation.
*/
define<
ArgsValidator extends PropertyValidators,
ReturnsValidator extends Validator<any, "required", any> | void,
ReturnValue extends ReturnValueForOptionalValidator<ReturnsValidator> = any,
>(
workflow: WorkflowDefinition<ArgsValidator, ReturnsValidator, ReturnValue>,
): RegisteredMutation<"internal", ObjectType<ArgsValidator>, void> {
return workflowMutation(
this.component,
workflow,
this.options?.workpoolOptions,
);
}
/**
* Kick off a defined workflow.
*
* @param ctx - The Convex context.
* @param workflow - The workflow to start (e.g. `internal.index.exampleWorkflow`).
* @param args - The workflow arguments.
* @returns The workflow ID.
*/
async start<F extends FunctionReference<"mutation", "internal">>(
ctx: RunMutationCtx,
workflow: F,
args: FunctionArgs<F>,
options?: CallbackOptions & {
/**
* By default, during creation the workflow will be initiated immediately.
* The benefit is that you catch errors earlier (e.g. passing a bad
* workflow reference or catch arg validation).
*
* If you set this to true, the workflow will be created but the run
* will be scheduled to run asynchronously.
* You can use this to make `start` faster (you still get a workflowId).
*/
validateAsync?: boolean;
},
): Promise<WorkflowId> {
const handle = await createFunctionHandle(workflow);
const onComplete = options?.onComplete
? {
fnHandle: await createFunctionHandle(options.onComplete),
context: options.context,
}
: undefined;
const workflowId = await ctx.runMutation(this.component.workflow.create, {
workflowName: getFunctionName(workflow),
workflowHandle: handle,
workflowArgs: args,
maxParallelism: this.options?.workpoolOptions?.maxParallelism,
onComplete,
validateAsync: options?.validateAsync,
});
return workflowId as unknown as WorkflowId;
}
/**
* Get a workflow's status.
*
* @param ctx - The Convex context.
* @param workflowId - The workflow ID.
* @returns The workflow status.
*/
async status(
ctx: RunQueryCtx,
workflowId: WorkflowId,
): Promise<WorkflowStatus> {
const { workflow, inProgress } = await ctx.runQuery(
this.component.workflow.getStatus,
{ workflowId },
);
const running = inProgress.map((entry) => entry.step);
switch (workflow.runResult?.kind) {
case undefined:
return { type: "inProgress", running };
case "canceled":
return { type: "canceled" };
case "failed":
return { type: "failed", error: workflow.runResult.error };
case "success":
return { type: "completed" };
}
}
/**
* Cancel a running workflow.
*
* @param ctx - The Convex context.
* @param workflowId - The workflow ID.
*/
async cancel(ctx: RunMutationCtx, workflowId: WorkflowId) {
await ctx.runMutation(this.component.workflow.cancel, {
workflowId,
});
}
/**
* Clean up a completed workflow's storage.
*
* @param ctx - The Convex context.
* @param workflowId - The workflow ID.
* @returns - Whether the workflow's state was cleaned up.
*/
async cleanup(ctx: RunMutationCtx, workflowId: WorkflowId): Promise<boolean> {
return await ctx.runMutation(this.component.workflow.cleanup, {
workflowId,
});
}
}
type RunQueryCtx = {
runQuery: GenericQueryCtx<GenericDataModel>["runQuery"];
};
type RunMutationCtx = {
runMutation: GenericMutationCtx<GenericDataModel>["runMutation"];
};