@convex-dev/workflow
Version:
Convex component for durably executing workflows.
116 lines • 5.48 kB
JavaScript
import { BaseChannel } from "async-channel";
import { assert } from "convex-helpers";
import { validate } from "convex-helpers/validators";
import { internalMutationGeneric } from "convex/server";
import { asObjectValidator, v, } from "convex/values";
import { createLogger } from "../component/logging.js";
import { setupEnvironment } from "./environment.js";
import { StepExecutor } from "./step.js";
import { StepContext } from "./stepContext.js";
import { checkArgs } from "./validator.js";
const workflowArgs = v.object({
workflowId: v.id("workflows"),
generationNumber: v.number(),
});
const INVALID_WORKFLOW_MESSAGE = `Invalid arguments for workflow: Did you invoke the workflow with ctx.runMutation() instead of workflow.start()?`;
// This function is defined in the calling component but then gets passed by
// function handle to the workflow component for execution. This function runs
// one "poll" of the workflow, replaying its execution from the journal until
// it blocks next.
export function workflowMutation(component, registered, defaultWorkpoolOptions) {
const workpoolOptions = {
...defaultWorkpoolOptions,
...registered.workpoolOptions,
};
return internalMutationGeneric({
handler: async (ctx, args) => {
if (!validate(workflowArgs, args)) {
throw new Error(INVALID_WORKFLOW_MESSAGE);
}
const { workflowId, generationNumber } = args;
const { workflow, inProgress, logLevel, journalEntries, ok } = await ctx.runQuery(component.journal.load, { workflowId });
const console = createLogger(logLevel);
if (!ok) {
console.error(`Failed to load journal for ${workflowId}`);
await ctx.runMutation(component.workflow.complete, {
workflowId,
generationNumber,
runResult: { kind: "failed", error: "Failed to load journal" },
now: Date.now(),
});
return;
}
if (workflow.generationNumber !== generationNumber) {
console.error(`Invalid generation number: ${generationNumber}`);
return;
}
if (workflow.runResult?.kind === "success") {
console.log(`Workflow ${workflowId} completed, returning.`);
return;
}
if (inProgress.length > 0) {
console.log(`Workflow ${workflowId} blocked by ` +
inProgress
.map((entry) => `${entry.step.name} (${entry._id})`)
.join(", "));
return;
}
for (const journalEntry of journalEntries) {
assert(!journalEntry.step.inProgress, `Assertion failed: not blocked but have in-progress journal entry`);
}
const channel = new BaseChannel(workpoolOptions.maxParallelism ?? 10);
const step = new StepContext(workflowId, channel);
const originalEnv = setupEnvironment(step);
const executor = new StepExecutor(workflowId, generationNumber, ctx, component, journalEntries, channel, originalEnv, workpoolOptions);
const handlerWorker = async () => {
let runResult;
try {
checkArgs(workflow.args, registered.args);
const returnValue = (await registered.handler(step, workflow.args)) ?? null;
runResult = { kind: "success", returnValue };
if (registered.returns) {
try {
validate(asObjectValidator(registered.returns), returnValue, {
throw: true,
});
}
catch (error) {
const message = error instanceof Error ? error.message : `${error}`;
runResult = {
kind: "failed",
error: "Invalid return value: " + message,
};
}
}
}
catch (error) {
runResult = { kind: "failed", error: error.message };
}
return { type: "handlerDone", runResult };
};
const executorWorker = async () => {
return await executor.run();
};
const result = await Promise.race([handlerWorker(), executorWorker()]);
switch (result.type) {
case "handlerDone": {
await ctx.runMutation(component.workflow.complete, {
workflowId,
generationNumber,
runResult: result.runResult,
now: originalEnv.Date.now(),
});
break;
}
case "executorBlocked": {
// Nothing to do, we already started steps in the StepExecutor.
break;
}
}
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
});
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const console = "THIS IS A REMINDER TO USE getDefaultLogger";
//# sourceMappingURL=workflowMutation.js.map