@convex-dev/workflow
Version:
Convex component for durably executing workflows.
187 lines • 7.9 kB
JavaScript
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