UNPKG

@convex-dev/workflow

Version:

Convex component for durably executing workflows.

187 lines 7.9 kB
import { getConvexSize, v } from "convex/values"; import { mutation, query } from "./_generated/server.js"; import { journalDocument, step, workflowDocument, } from "./schema.js"; import { getWorkflow } from "./model.js"; import { logLevel } from "./logging.js"; import { vRetryBehavior } from "@convex-dev/workpool"; import { getWorkpool, workpoolOptions, } from "./pool.js"; import { internal } from "./_generated/api.js"; import { createFunctionHandle } from "convex/server"; import { getDefaultLogger } from "./utils.js"; import { assert } from "convex-helpers"; import { MAX_JOURNAL_SIZE } from "../shared.js"; import { awaitEvent } from "./event.js"; import { createHandler } from "./workflow.js"; export const load = query({ args: { workflowId: v.id("workflows"), shortCircuit: v.optional(v.boolean()), }, returns: v.object({ workflow: workflowDocument, journalEntries: v.array(journalDocument), ok: v.boolean(), logLevel, blocked: v.optional(v.boolean()), }), handler: async (ctx, { workflowId, shortCircuit }) => { const workflow = await ctx.db.get("workflows", workflowId); assert(workflow, `Workflow not found: ${workflowId}`); const { logLevel } = await getDefaultLogger(ctx); const journalEntries = []; let journalSize = 0; if (shortCircuit) { const inProgress = await ctx.db .query("steps") .withIndex("inProgress", (q) => q.eq("step.inProgress", true).eq("workflowId", workflowId)) .first(); if (inProgress) { return { journalEntries: [inProgress], blocked: true, workflow, logLevel, ok: true, }; } } for await (const entry of ctx.db .query("steps") .withIndex("workflow", (q) => q.eq("workflowId", workflowId))) { journalEntries.push(entry); journalSize += getConvexSize(entry); if (journalSize > MAX_JOURNAL_SIZE) { return { journalEntries, workflow, logLevel, ok: false }; } } return { journalEntries, workflow, logLevel, ok: true }; }, }); export const startSteps = mutation({ args: { workflowId: v.id("workflows"), generationNumber: v.number(), steps: v.array(v.object({ step, retry: v.optional(v.union(v.boolean(), vRetryBehavior)), schedulerOptions: v.optional(v.union(v.object({ runAt: v.optional(v.number()) }), v.object({ runAfter: v.optional(v.number()) }))), })), workpoolOptions: v.optional(workpoolOptions), }, returns: v.array(journalDocument), handler: async (ctx, args) => { const { generationNumber } = args; const workflow = await getWorkflow(ctx, args.workflowId, generationNumber); const console = await getDefaultLogger(ctx); if (workflow.runResult !== undefined) { throw new Error(`Workflow not running: ${args.workflowId}`); } const maxEntry = await ctx.db .query("steps") .withIndex("workflow", (q) => q.eq("workflowId", workflow._id)) .order("desc") .first(); const stepNumberBase = maxEntry ? maxEntry.stepNumber + 1 : 0; const workpool = await getWorkpool(ctx, args.workpoolOptions); const onComplete = internal.pool.onComplete; const entries = await Promise.all(args.steps.map(async (stepArgs, index) => { const { retry, schedulerOptions } = stepArgs; const stepNumber = stepNumberBase + index; const stepId = await ctx.db.insert("steps", { workflowId: workflow._id, stepNumber, step: stepArgs.step, }); let entry = await ctx.db.get("steps", stepId); assert(entry, "Step not found"); const step = entry.step; const { name } = step; console.event("started", { workflowId: workflow._id, workflowName: workflow.name, stepName: name, stepNumber, }); if (step.kind === "event") { // Note: This modifies entry in place as well. entry = await awaitEvent(ctx, entry, { name, eventId: step.args.eventId, }); if (step.runResult) { console.event("eventConsumed", { workflowId: entry.workflowId, workflowName: workflow.name, status: step.runResult.kind, eventName: step.name, stepNumber: stepNumber, durationMs: step.completedAt - step.startedAt, }); } } else if (step.kind === "workflow") { const workflowId = await createHandler(ctx, { workflowName: step.name, workflowHandle: step.handle, workflowArgs: step.args, maxParallelism: args.workpoolOptions?.maxParallelism, onComplete: { fnHandle: await createFunctionHandle(internal.pool.nestedWorkflowOnComplete), context: { stepId, generationNumber, workpoolOptions: args.workpoolOptions, }, }, startAsync: true, }); step.workflowId = workflowId; } else if (step.runResult) { // Already completed inline by the caller — nothing to enqueue. console.event("stepCompleted", { workflowId: entry.workflowId, workflowName: workflow.name, status: step.runResult.kind, stepName: step.name, stepNumber: stepNumber, }); } else if (step.kind === "sleep") { const context = { generationNumber, stepId, workpoolOptions: args.workpoolOptions, }; step.workId = await workpool.enqueueQuery(ctx, internal.workflow.sleep, {}, { context, onComplete, name, ...schedulerOptions }); } else { const context = { generationNumber, stepId, workpoolOptions: args.workpoolOptions, }; let workId; switch (step.functionType) { case "query": { workId = await workpool.enqueueQuery(ctx, step.handle, step.args, { context, onComplete, name, ...schedulerOptions }); break; } case "mutation": { workId = await workpool.enqueueMutation(ctx, step.handle, step.args, { context, onComplete, name, ...schedulerOptions }); break; } case "action": { workId = await workpool.enqueueAction(ctx, step.handle, step.args, { context, onComplete, name, retry, ...schedulerOptions }); break; } } step.workId = workId; } await ctx.db.replace("steps", entry._id, entry); return entry; })); return entries; }, }); //# sourceMappingURL=journal.js.map