diff --git a/www/apps/book/app/advanced-development/workflows/access-workflow-errors/page.mdx b/www/apps/book/app/advanced-development/workflows/access-workflow-errors/page.mdx new file mode 100644 index 0000000000..0ff6503255 --- /dev/null +++ b/www/apps/book/app/advanced-development/workflows/access-workflow-errors/page.mdx @@ -0,0 +1,52 @@ +export const metadata = { + title: `${pageNumber} Access Workflow Errors`, +} + +# {metadata.title} + +In this document, you’ll learn how to access errors that occur during a workflow’s execution. + +## How to Access Workflow Errors + +By default, when an error occurs in a workflow, it throws that error, and the execution stops. + +You can configure the workflow to return the errors instead so that you can access and handle them differently. + +For example: + +export const highlights = [ + ["11", "errors", "`errors` is an array of errors that occur during the workflow's execution."], + ["14", "throwOnError", "Specify that errors occuring during the workflow's execution should be returned, not thrown."], +] + +```ts title="src/api/store/workflows/route.ts" highlights={highlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/medusa" +import myWorkflow from "../../../workflows/hello-world" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result, errors } = await myWorkflow(req.scope) + .run({ + // ... + throwOnError: false, + }) + + if (errors.length) { + return res.send({ + errors: errors.map((error) => error.error), + }) + } + + res.send(result) +} + +``` + +The object passed to the `run` method accepts a `throwOnError` property. When disabled, the errors are returned in the `errors` property of the `run`'s output. The value of `errors` is an array of error objects. + +Then, you can check the items in the `errors` array and handle them accordingly. Each error object has an `error` property, which holds the name or the text of the thrown error. \ No newline at end of file diff --git a/www/apps/book/app/advanced-development/workflows/compensation-function/page.mdx b/www/apps/book/app/advanced-development/workflows/compensation-function/page.mdx index b8272e3daf..1be6785c81 100644 --- a/www/apps/book/app/advanced-development/workflows/compensation-function/page.mdx +++ b/www/apps/book/app/advanced-development/workflows/compensation-function/page.mdx @@ -8,13 +8,19 @@ In this chapter, you'll learn how to add a compensation function to a step. ## Compensation Function -Errors can occur in a workflow. To avoid data inconsistency, you can pass a compensation function as a third parameter to the `createStep` function. +Errors can occur in a workflow. To avoid data inconsistency, define a function to run when an error occurs in a step. This function is called the compensation function. -The compensation function only runs if an error occurs throughout the Workflow. It’s useful to undo or roll back actions you’ve performed in a step. +Each step can have a compensation function. The compensation function only runs if an error occurs throughout the Workflow. It’s useful to undo or roll back actions you’ve performed in a step. For example, change step one to add a compensation function and step two to throw an error: -```ts title="src/workflows/hello-world.ts" highlights={[["10"], ["11"], ["12"]]} +```ts title="src/workflows/hello-world.ts" highlights={[["16"], ["17"], ["18"]]} +// other imports... +import { + createStep, + StepResponse, +} from "@medusajs/workflows-sdk" + const step1 = createStep( "step-1", async () => { @@ -31,7 +37,7 @@ const step1 = createStep( const step2 = createStep( "step-2", - async ({ name }: WorkflowInput) => { + async () => { throw new Error("Throwing an error...") } ) @@ -54,12 +60,11 @@ type WorkflowOutput = { } const myWorkflow = createWorkflow< - WorkflowInput, + {}, WorkflowOutput >("hello-world", function (input) { const str1 = step1() - // to pass input - const str2 = step2(input) + step2() return { message: str1, @@ -83,11 +88,7 @@ export async function GET( res: MedusaResponse ) { const { result } = await myWorkflow(req.scope) - .run({ - input: { - name: req.query.name as string, - }, - }) + .run() res.send(result) } diff --git a/www/apps/book/app/advanced-development/workflows/long-running-workflow/page.mdx b/www/apps/book/app/advanced-development/workflows/long-running-workflow/page.mdx new file mode 100644 index 0000000000..7476538301 --- /dev/null +++ b/www/apps/book/app/advanced-development/workflows/long-running-workflow/page.mdx @@ -0,0 +1,156 @@ +import { TypeList } from "docs-ui" + +export const metadata = { + title: `${pageNumber} Long-Running Workflows`, +} + +# {metadata.title} + +In this chapter, you’ll learn what a long-running workflow is and how to configure it. + +## What is a Long-Running Workflow? + +By default, when you execute a workflow, you wait until the workflow finishes execution. Once you receive the workflow’s output, the rest of the code is executed. + +A long-running workflow is a workflow that continues its execution in the background. You don’t receive its output immediately. Instead, you subscribe to the workflow execution to listen to status changes and receive its result once the execution is finished. + +--- + +## Configure Long-Running Workflows + +A workflow is considered long-running if one or more of its steps have their `async` configuration set to `true`. + +For example, consider the following workflow and steps: + +```ts title="src/workflows/hello-world.ts" highlights={[["13"]]} +import { + createStep, + createWorkflow, +} from "@medusajs/workflows-sdk" + +const step1 = createStep("step-1", async () => { + // ... +}) + +const step2 = createStep( + { + name: "step-2", + async: true, + }, + async () => { + // ... + } +) + +const step3 = createStep("step-3", async () => { + // ... +}) + +type WorkflowOutput = { + message: string +} + +const myWorkflow = createWorkflow< + {}, + WorkflowOutput +>({ + name: "hello-world", +}, function () { + step1() + step2() + step3() +}) + +export default myWorkflow + +``` + +The second step has in its configuration object `async` set to true. This indicates that this step is an asynchronous step. + +So, when you execute the `hello-world` workflow, it continues its execution in the background once it reaches the second step. + +--- + +## Access Long-Running Workflow Status and Result + + + +- [A workflow engine module installed](!resources!/architectural-modules/workflow-engine/in-memory). + + + +To access the status and result of a long-running workflow, use the workflow engine registered in the Medusa Container. The workflow engine provides methods to access and subscribe to workflow executions. + +For example: + +export const highlights = [ + ["18", "", "Resolve the workflow engine from the Medusa container."], + ["24", "subscribe", "Subscribe to status changes of the workflow execution."], +] + +```ts title="src/api/store/workflows/route.ts" highlights={highlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/medusa" +import myWorkflow from "../../../workflows/hello-world" +import { + IWorkflowEngineService, +} from "@medusajs/workflows-sdk" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { transaction, result } = await myWorkflow(req.scope) + .run() + + const workflowEngine = req.scope.resolve< + IWorkflowEngineService + >( + ModuleRegistrationName.WORKFLOW_ENGINE + ) + + await workflowEngine.subscribe({ + workflowId: "hello-world", + transactionId: transaction.transactionId, + subscriber: (data) => { + if (data.eventType === "onFinish") { + console.log("Finished execution", data.result) + } else if (data.eventType === "onStepFailure") { + console.log("Workflow failed", data.step) + } + }, + }) + + res.send(result) +} +``` + +In the above example, you execute the long-running workflow `hello-world`. You then resolve the workflow engine from the Medusa container and use its `subscribe` method to listen to changes in the workflow execution’s status. + +The `subscribe` method accepts an object having three properties: + + + +Once the workflow execution finishes, the subscriber function is executed with the `eventType` of the received parameter set to `onFinish`. The workflow’s output is set in the `result` property of the parameter. diff --git a/www/apps/book/app/advanced-development/workflows/parallel-steps/page.mdx b/www/apps/book/app/advanced-development/workflows/parallel-steps/page.mdx index c3bad318b9..d8cd4049d9 100644 --- a/www/apps/book/app/advanced-development/workflows/parallel-steps/page.mdx +++ b/www/apps/book/app/advanced-development/workflows/parallel-steps/page.mdx @@ -1,5 +1,5 @@ export const metadata = { - title: `${pageNumber} Running Workflow Steps in Parallel`, + title: `${pageNumber} Run Workflow Steps in Parallel`, } # {metadata.title} diff --git a/www/apps/book/app/advanced-development/workflows/retry-failed-steps/page.mdx b/www/apps/book/app/advanced-development/workflows/retry-failed-steps/page.mdx new file mode 100644 index 0000000000..9d03968600 --- /dev/null +++ b/www/apps/book/app/advanced-development/workflows/retry-failed-steps/page.mdx @@ -0,0 +1,87 @@ +export const metadata = { + title: `${pageNumber} Retry Failed Steps`, +} + +# {metadata.title} + +In this chapter, you’ll learn how to configure steps to allow retrial on failure. + +## Configure a Step’s Retrial + +By default, when an error occurs in a step, the step and the workflow fail, and the execution stops. + +You can configure the step to retry on failure. The `createStep` function can accept a configuration object instead of the step’s name as a first parameter: + +```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} +import { + createStep, + StepResponse, + createWorkflow, +} from "@medusajs/workflows-sdk" + +const step1 = createStep( + { + name: "step-1", + maxRetries: 2, + }, + async () => { + console.log("Executing step 1") + + throw new Error("Oops! Something happened.") + } +) + +type WorkflowOutput = { + message: string +} + +const myWorkflow = createWorkflow< + {}, + WorkflowOutput +>("hello-world", function () { + const str1 = step1() + + return { + message: str1, + } +}) + +export default myWorkflow +``` + +The step’s configuration object accepts a `maxRetries` property, which is a number indicating the number of times a step can be retried when it fails. + +When you execute the above workflow, you’ll see the following result in the terminal: + +```bash +Executing step 1 +Executing step 1 +Executing step 1 +error: Oops! Something happened. +Error: Oops! Something happened. +``` + +The first line indicates the first time the step was executed, and the next two lines indicate the times the step was retried. After that, the step and workflow fail. + +--- + +## Step Retry Intervals + +By default, a step is retried immediately after it fails. + +To specify a wait time before a step is retried, pass a `retryInterval` property to the step's configuration object. Its value is a number of seconds to wait before retrying the step. + +For example: + +```ts title="src/workflows/hello-world.ts" highlights={[["5"]]} +const step1 = createStep( + { + name: "step-1", + maxRetries: 2, + retryInterval: 2, // 2 seconds + }, + async () => { + // ... + } +) +``` diff --git a/www/apps/book/app/advanced-development/workflows/workflow-timeout/page.mdx b/www/apps/book/app/advanced-development/workflows/workflow-timeout/page.mdx new file mode 100644 index 0000000000..1deede134d --- /dev/null +++ b/www/apps/book/app/advanced-development/workflows/workflow-timeout/page.mdx @@ -0,0 +1,86 @@ +export const metadata = { + title: `${pageNumber} Workflow Timeout`, +} + +# {metadata.title} + +In this chapter, you’ll learn how to set a timeout for workflows and steps. + +## Configure Workflow Timeout + +By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. + +You can configure a workflow’s timeout to indicate how long the workflow can run. Once the specified time is passed and the workflow is still running, the workflow is considered failed and an error is thrown. + +For example: + +```ts title="src/workflows/hello-world.ts" highlights={[["22"]]} +import { + createStep, + createWorkflow, +} from "@medusajs/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + // ... + } +) + +type WorkflowOutput = { + message: string +} + +const myWorkflow = createWorkflow< + {}, + WorkflowOutput +>({ + name: "hello-world", + timeout: 2, // 2 seconds +}, function () { + const str1 = step1() + + return { + message: str1, + } +}) + +export default myWorkflow + +``` + +The `createWorkflow` function can accept a configuration object instead of the workflow’s name. In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. + + + +A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](../access-workflow-errors/page.mdx). The error’s name is `TransactionTimeoutError`. + + + +--- + +## Configure Step Timeout + +Alternatively, you can configure timeout for a step rather than the entire workflow. + +For example: + +```tsx +const step1 = createStep( + { + name: "step-1", + timeout: 2, // 2 seconds + }, + async () => { + // ... + } +) +``` + +The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. + + + +A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](../access-workflow-errors/page.mdx). The error’s name is `TransactionStepTimeoutError`. + + diff --git a/www/apps/book/sidebar.mjs b/www/apps/book/sidebar.mjs index 35c1cfee45..e1ba58f36a 100644 --- a/www/apps/book/sidebar.mjs +++ b/www/apps/book/sidebar.mjs @@ -194,9 +194,25 @@ export const sidebar = sidebarAttachHrefCommonOptions( path: "/advanced-development/workflows/advanced-example", title: "Example: Advanced Workflow", }, + { + path: "/advanced-development/workflows/access-workflow-errors", + title: "Access Workflow Errors", + }, + { + path: "/advanced-development/workflows/retry-failed-steps", + title: "Retry Failed Steps", + }, { path: "/advanced-development/workflows/parallel-steps", - title: "Running Steps in Parallel", + title: "Run Steps in Parallel", + }, + { + path: "/advanced-development/workflows/workflow-timeout", + title: "Workflow Timeout", + }, + { + path: "/advanced-development/workflows/long-running-workflow", + title: "Long-Running Workflow", }, ], }, diff --git a/www/apps/resources/app/architectural-modules/page.mdx b/www/apps/resources/app/architectural-modules/page.mdx index 28218723dd..92bcf7d0de 100644 --- a/www/apps/resources/app/architectural-modules/page.mdx +++ b/www/apps/resources/app/architectural-modules/page.mdx @@ -6,6 +6,6 @@ export const metadata = { # {metadata.title} -This section includes documentation for official Medusa architectural plugins. +This section includes documentation for official Medusa architectural modules. \ No newline at end of file diff --git a/www/apps/resources/app/architectural-modules/workflow-engine/in-memory/page.mdx b/www/apps/resources/app/architectural-modules/workflow-engine/in-memory/page.mdx new file mode 100644 index 0000000000..0ab6da7ae6 --- /dev/null +++ b/www/apps/resources/app/architectural-modules/workflow-engine/in-memory/page.mdx @@ -0,0 +1,39 @@ +import { Table } from "docs-ui" + +export const metadata = { + title: `In-Memory Workflow Engine Module`, +} + +# {metadata.title} + +The In-Memory Workflow Engine Module uses a plain JavaScript Map object to store the workflow executions. + +This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production. + +For production, it’s recommended to use modules like [Redis Workflow Engine Module](../redis/page.mdx). + +--- + +## Install the In-Memory Workflow Engine Module + +To install the In-Memory Workflow Engine Module, run the following command in the directory of your Medusa application: + +```bash npm2yarn +npm install @medusajs/workflow-engine-inmemory +``` + +Next, add the module into the `modules` property of the exported object in `medusa-config.js`: + +```js title="medusa-config.js" +const { Modules } = require("@medusajs/modules-sdk") + +// ... + +module.exports = { + // ... + modules: { + // ... + [Modules.WORKFLOW_ENGINE]: true, + }, +} +``` diff --git a/www/apps/resources/app/architectural-modules/workflow-engine/redis/page.mdx b/www/apps/resources/app/architectural-modules/workflow-engine/redis/page.mdx new file mode 100644 index 0000000000..7a0ee2ddc0 --- /dev/null +++ b/www/apps/resources/app/architectural-modules/workflow-engine/redis/page.mdx @@ -0,0 +1,182 @@ +import { Table } from "docs-ui" + +export const metadata = { + title: `Redis Workflow Engine Module`, +} + +# {metadata.title} + +The Redis Workflow Engine Module uses Redis to track workflow executions and handle their subscribers. In production, it's recommended to use this module. + +--- + +## Install the Redis Workflow Engine Module + + + +- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/). + + + +To install Redis Workflow Engine Module, run the following command in the directory of your Medusa application: + +```bash npm2yarn +npm install @medusajs/workflow-engine-redis +``` + +Next, add the module into the `modules` property of the exported object in `medusa-config.js`: + +export const highlights = [ + ["8", "redisUrl", "The Redis connection URL."] +] + +```js title="medusa-config.js" highlights={highlights} +const { Modules } = require("@medusajs/modules-sdk") + +// ... + +module.exports = { + // ... + modules: { + // ... + [Modules.WORKFLOW_ENGINE]: { + resolve: "@medusajs/workflow-engine-redis", + options: { + redis: { + url: process.emv.WE_REDIS_URL, + }, + }, + }, + }, +} +``` + +### Redis Workflow Engine Module Options + + + + + Option + Description + Required + Default + + + + + + + `url` + + + + + A string indicating the Redis connection URL. + + + + + No. If not provided, you must provide the `pubsub` option. + + + + + \- + + + + + + + `options` + + + + + An object of Redis options. Refer to the [Redis API Reference](https://redis.github.io/ioredis/index.html#RedisOptions) for details on accepted properties. + + + + + No + + + + + \- + + + + + + + `queueName` + + + + + The name of the queue used to keep track of retries and timeouts. + + + + + No + + + + + `medusa-workflows` + + + + + + + `pubsub` + + + + + A connection object having the following properties: + + - `url`: A required string indicating the Redis connection URL. + - `options`: An optional object of Redis options. Refer to the [Redis API Reference](https://redis.github.io/ioredis/index.html#RedisOptions) for details on accepted properties. + + + + + No. If not provided, you must provide the `url` option. + + + + + \- + + + + +
+ +### Environment Variables + +Make sure to add the following environment variables: + +```bash +WE_REDIS_URL= +``` + +--- + +## Test the Module + +To test the module, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +You'll see the following message in the terminal's logs: + +```bash noCopy noReport +Connection to Redis in module 'workflow-engine-redis' established +``` diff --git a/www/apps/resources/generated/files-map.mjs b/www/apps/resources/generated/files-map.mjs index a5de656e55..25de30859f 100644 --- a/www/apps/resources/generated/files-map.mjs +++ b/www/apps/resources/generated/files-map.mjs @@ -1,8 +1,4 @@ export const filesMap = [ - { - "filePath": "/www/apps/resources/app/.DS_Store", - "pathname": "/" - }, { "filePath": "/www/apps/resources/app/admin-widget-injection-zones/page.mdx", "pathname": "/admin-widget-injection-zones" @@ -28,8 +24,12 @@ export const filesMap = [ "pathname": "/architectural-modules" }, { - "filePath": "/www/apps/resources/app/commerce-modules/.DS_Store", - "pathname": "/commerce-modules" + "filePath": "/www/apps/resources/app/architectural-modules/workflow-engine/in-memory/page.mdx", + "pathname": "/architectural-modules/workflow-engine/in-memory" + }, + { + "filePath": "/www/apps/resources/app/architectural-modules/workflow-engine/redis/page.mdx", + "pathname": "/architectural-modules/workflow-engine/redis" }, { "filePath": "/www/apps/resources/app/commerce-modules/api-key/examples/page.mdx", diff --git a/www/apps/resources/generated/sidebar.mjs b/www/apps/resources/generated/sidebar.mjs index ff4e6b201a..e8711fba4a 100644 --- a/www/apps/resources/generated/sidebar.mjs +++ b/www/apps/resources/generated/sidebar.mjs @@ -6844,6 +6844,28 @@ export const generatedSidebar = [ "children": [] } ] + }, + { + "loaded": true, + "isPathHref": true, + "title": "Workflow Engine Modules", + "hasTitleStyling": true, + "children": [ + { + "loaded": true, + "isPathHref": true, + "path": "/architectural-modules/workflow-engine/in-memory", + "title": "In-Memory", + "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "path": "/architectural-modules/workflow-engine/redis", + "title": "Redis", + "children": [] + } + ] } ] }, diff --git a/www/apps/resources/sidebar.mjs b/www/apps/resources/sidebar.mjs index d507d47f79..36c4ae636e 100644 --- a/www/apps/resources/sidebar.mjs +++ b/www/apps/resources/sidebar.mjs @@ -1619,6 +1619,20 @@ export const sidebar = sidebarAttachHrefCommonOptions([ }, ], }, + { + title: "Workflow Engine Modules", + hasTitleStyling: true, + children: [ + { + path: "/architectural-modules/workflow-engine/in-memory", + title: "In-Memory", + }, + { + path: "/architectural-modules/workflow-engine/redis", + title: "Redis", + }, + ], + }, ], }, { diff --git a/www/packages/eslint-config-docs/content.js b/www/packages/eslint-config-docs/content.js index 6c9adc8bbe..2cde3e3498 100644 --- a/www/packages/eslint-config-docs/content.js +++ b/www/packages/eslint-config-docs/content.js @@ -110,7 +110,8 @@ module.exports = { "@typescript-eslint/no-var-requires": "off", "prefer-rest-params": "off", "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-non-null-asserted-optional-chain": "off" + "@typescript-eslint/no-non-null-asserted-optional-chain": "off", + "@typescript-eslint/ban-types": "off" }, }, ],