diff --git a/docs/getting_started/typescript/hello_world_in_typescript/images/activity_stack_trace.png b/docs/getting_started/typescript/hello_world_in_typescript/images/activity_stack_trace.png new file mode 100644 index 00000000..5c16643a Binary files /dev/null and b/docs/getting_started/typescript/hello_world_in_typescript/images/activity_stack_trace.png differ diff --git a/docs/getting_started/typescript/hello_world_in_typescript/images/history.png b/docs/getting_started/typescript/hello_world_in_typescript/images/history.png new file mode 100644 index 00000000..b9de1ae8 Binary files /dev/null and b/docs/getting_started/typescript/hello_world_in_typescript/images/history.png differ diff --git a/docs/getting_started/typescript/hello_world_in_typescript/images/history_failed.png b/docs/getting_started/typescript/hello_world_in_typescript/images/history_failed.png new file mode 100644 index 00000000..d964eec2 Binary files /dev/null and b/docs/getting_started/typescript/hello_world_in_typescript/images/history_failed.png differ diff --git a/docs/getting_started/typescript/hello_world_in_typescript/images/history_recovered.png b/docs/getting_started/typescript/hello_world_in_typescript/images/history_recovered.png new file mode 100644 index 00000000..c5476b81 Binary files /dev/null and b/docs/getting_started/typescript/hello_world_in_typescript/images/history_recovered.png differ diff --git a/docs/getting_started/typescript/hello_world_in_typescript/images/overview.png b/docs/getting_started/typescript/hello_world_in_typescript/images/overview.png new file mode 100644 index 00000000..086eb551 Binary files /dev/null and b/docs/getting_started/typescript/hello_world_in_typescript/images/overview.png differ diff --git a/docs/getting_started/typescript/hello_world_in_typescript/images/timeline_failed.png b/docs/getting_started/typescript/hello_world_in_typescript/images/timeline_failed.png new file mode 100644 index 00000000..077be7b3 Binary files /dev/null and b/docs/getting_started/typescript/hello_world_in_typescript/images/timeline_failed.png differ diff --git a/docs/getting_started/typescript/hello_world_in_typescript/index.md b/docs/getting_started/typescript/hello_world_in_typescript/index.md index 9ae3e37b..b2d1b566 100644 --- a/docs/getting_started/typescript/hello_world_in_typescript/index.md +++ b/docs/getting_started/typescript/hello_world_in_typescript/index.md @@ -6,7 +6,8 @@ keywords: [typescript, javascript, temporal, sdk, tutorial, learn] description: In this tutorial you will build a Temporal Application using the TypeScript SDK. You'll write a Workflow, an Activity, and define a Worker. tags: [TypeScript, SDK] last_update: - date: 2022-12-20 + author: Brian P. Hogan + date: 2024-12-18 image: /img/temporal-logo-twitter-card.png --- @@ -17,67 +18,136 @@ image: /img/temporal-logo-twitter-card.png - **Level:** ⭐ Temporal beginner - **Time:** ⏱️ ~15 minutes - **Goals:** 🙌 - - Set up, build, and test a Temporal application project from scratch using the [TypeScript SDK](https://github.com/temporalio/sdk-typescript). + - Set up, build, and test a Temporal Application project from scratch using the [TypeScript SDK](https://github.com/temporalio/sdk-typescript). - Identify the four parts of a Temporal Workflow application. - Describe how the Temporal Server gets information to the Worker. + - Observe how Temporal recovers from failed Activities. ::: ### Introduction -Creating reliable applications is a difficult task. [Temporal](https://temporal.io) lets you create fault-tolerant, resilient applications using programming languages you already know, so you can build complex applications that execute reliably and recover from failures. +Creating reliable applications is a difficult chore. [Temporal](https://temporal.io/) lets you create fault-tolerant, resilient applications using programming languages you already know, so you can build complex applications that execute reliably and recover from failures. -In this tutorial, you will build your first Temporal Application from scratch using the [Temporal TypeScript SDK](https://github.com/temporalio/sdk-typescript). The app will consist of four pieces: +In this tutorial, you will build your first Temporal Application from scratch using the [Temporal TypeScript SDK](https://github.com/temporalio/sdk-typescript). You'll develop a small application that asks for your name and then uses APIs to get your public IP address and your location based on that address. External requests can fail due to rate limiting, network interruptions, or other errors. Using Temporal for this application will let you automatically recover from these and other kinds of failures without having to write explicit error-handling code. -1. A [Workflow](https://docs.temporal.io/workflows): Workflows are functions that define the overall flow of the application. -2. An [Activity](https://docs.temporal.io/activities): Activities are functions called by Workflows, and they contain any logic that might fail or behave differently at different times. The Workflow you'll create executes a single Activity that takes a string as input and returns a formatted version of this string to the Workflow. -3. A [Worker](https://docs.temporal.io/workers): Workers host the Activity and Workflow code and executes the code piece by piece. -4. An initiator: To start a Workflow, you need to create a Client instance and call the `.start()` method. You'll write a separate script to do this. +The app will consist of the following pieces: -You'll also write a unit test to ensure your Workflow executes successfully. +1. Two [Activities](https://docs.temporal.io/activities): Activities are functions called by Workflows, and they contain any logic that might fail or behave differently at different times. The first Activity will get your IP address, and the second Activity will use that IP address to get your location. -When you're done, you'll have a basic application and a clear understanding of how to build out the components you'll need in future Temporal applications. +1. A [Workflow](https://docs.temporal.io/workflows): Workflows are functions that define the overall flow of the application. Your Workflow will execute both Activities you define. It will call the first Activity to fetch the IP address, and then use the result of that Activity to call the next Activity to find your location. +2. A [Worker](https://docs.temporal.io/workers): Workers host the Activity and Workflow code and execute the code piece by piece. +3. A client program: You'll need to start your Worfklow. In this tutorial you'll create a small program using the [Temporal Client](https://docs.temporal.io/encyclopedia/temporal-sdks#temporal-client) to start the Workflow. -All of the code in this tutorial is available in the [hello-world](https://github.com/temporalio/samples-typescript/tree/main/hello-world) directory of the `temporalio/samples-typescript` repository. +You'll also write tests to ensure your Workflow executes successfully. + +When you're done, you'll have a basic application and a clear understanding of how to build out the components you'll need in future Temporal applications in TypeScript. ## Prerequisites Before starting this tutorial: -- [Set up a local development environment for developing Temporal applications using Node.js and TypeScript](/getting_started/typescript/dev_environment/index.md) -- Follow the tutorial [Run your first Temporal application with the TypeScript SDK](/getting_started/typescript/first_program_in_typescript/) to gain a better understanding of what Temporal is and how its components fit together. +- [Set up a local development environment for developing Temporal applications using Node.js and TypeScript](https://learn.temporal.io/getting_started/typescript/dev_environment/) + - Ensure that the Temporal Service is running in a terminal on your local machine and that you can access the Temporal Web UI. In this tutorial you'll use port `8233` for the Web UI, which is the default port. -The TypeScript SDK requires Node.js 18 or later. +- Follow the tutorial [Run your first Temporal application with the TypeScript SDK](https://learn.temporal.io/getting_started/typescript/first_program_in_typescript/) to gain a better understanding of what Temporal is and how its components fit together. -## Create a new project +## Create a New Temporal TypeScript Project -This is a step by step guide that shows you how to create a project from scratch, and the goal is for you to better understand the structure of a Temporal application by creating the files on your own. +While you could create a new directory, initialize a TypeScript project, and configure things manually, the TypeScript SDK offers a project creation tool you can use to create a project folder and set up dependencies. -This project exists in its entirety in the [Temporal TypeScript samples repo](https://github.com/temporalio/samples-typescript/tree/main/hello-world). To skip these manual steps and get the project running locally, use the [package initializer](https://docs.temporal.io/typescript/package-initializer#optional-flags) command `npx @temporalio/create@latest hello-world --sample hello-world` +Run the following command in your shell: -To build an app with the Temporal TypeScript SDK, you'll create a new directory and use `npm` to initialize it as a Node.js project. Then you'll add the Temporal SDK packages and other dependencies to your project. +```command +npx @temporalio/create --sample empty temporal-ip-geolocation +``` -In a terminal, create a new project directory called `hello-world`: +The command will ask you to confirm if you want to install `@temporalio/create`: + +```output +Need to install the following packages: +@temporalio/create@1.11.5 +Ok to proceed? (y) y +``` + +Enter `y` to continue. The process will then create your app: + +```output +Creating a new Temporal project in /Users/temporal/temporal-ip-geolocation/ + +Downloading files for sample empty. This might take a moment. + +Installing packages. This might take a couple of minutes. +``` + +It'll ask you if you'd like to create a Git repository: -```command -mkdir hello-world ``` +✔ Would you like me to initialize a git repository for the project? … yes +Initialized a git repository. +``` + +Then it'll give you further instructions, including how to set up and start a local Temporal service and install a compatible version of NodeJS: + +```output +Success! Created project temporal-ip-geolocation at: -Switch to the new directory: +/Users/temporal/temporal-ip-geolocation/ + +To begin development, install the Temporal CLI: + +Mac: brew install temporal +Other: Download and extract the latest release from https://github.com/temporalio/cli/releases/latest + +Start Temporal Server: + +temporal server start-dev + +Use Node version 18+ (v22.x is recommended): + +Mac: brew install node@22 +Other: https://nodejs.org/en/download/ +``` + +Finally, you'll see how to run the Worker and Workflow, but don't do that yet. + +Once the command completes, switch to the `temporal-ip-gelocation` folder: ```command -cd hello-world +cd temporal-ip-geolocation ``` -Create the `package.json` file in the root of your project and add the following code to the file that defines the project, sets up scripts, and defines the dependencies this project requires: +The generator created the following files and folders: -[package.json](https://github.com/temporalio/samples-typescript/blob/main/hello-world/package.json) +```output +├── README.md +├── node_modules +├── package-lock.json +├── package.json +├── src +│ ├── activities.ts +│ ├── client.ts +│ ├── mocha +│ ├── worker.ts +│ └── workflows.ts +└── tsconfig.json +``` + +Here's what each file does: + +- The `package.json` file holds the project dependencies and a handful of scripts you'll use to run Workflows, Workflows, tests, and other tasks like linting and formatting your code. +- The `tsconfig.json` file holds the TypeScript configuration designed for working with Temporal's SDK. +- The `src/activities.ts` file is where you can define Activities. +- The `src/client.ts` file has the code for a small CLI program to execute a Workflow. You won't use this directly in this guide. +- The `src/mocha` folder is where you'll place your tests. We recommend using [Mocha](https://mochajs.org/) to test your Temporal Workflows and Activities. +- The `src/workflows.ts` file is where you can define Workflows. +- The `src/worker.ts` file has the code to configure and run your Worker process, which executes your Workflows and Activities. + +You should review a few parts of the `package.json` before moving on. + +First, review the `scripts` section. These are the `npm` commands you'll use to build, lint, test, and start your application code: ```json -{ - "name": "temporal-hello-world", - "version": "0.1.0", - "private": true, "scripts": { "build": "tsc --build", "build.watch": "tsc --build --watch", @@ -88,142 +158,177 @@ Create the `package.json` file in the root of your project and add the following "format": "prettier --config .prettierrc 'src/**/*.ts' --write", "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" }, - "nodemonConfig": { - "execMap": { - "ts": "ts-node" - }, - "ext": "ts", - "watch": [ - "src" - ] - }, - "dependencies": { - "@temporalio/activity": "^1.9.0", - "@temporalio/client": "^1.9.0", - "@temporalio/worker": "^1.9.0", - "@temporalio/workflow": "^1.9.0", - "nanoid": "3.x" - }, - "devDependencies": { - "@temporalio/testing": "^1.9.0", - "@tsconfig/node16": "^1.0.0", - "@types/mocha": "8.x", - "@types/node": "^16.11.43", - "@typescript-eslint/eslint-plugin": "^5.0.0", - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^7.32.0", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-deprecation": "^1.2.1", - "mocha": "8.x", - "nodemon": "^2.0.12", - "prettier": "^2.8.8", - "ts-node": "^10.8.1", - "typescript": "^4.4.2" - } -} ``` -You should review a few parts of the `package.json`, and the first is the `scripts` section. These are the `npm` commands you'll use to build, lint, and start your application code. - Next, examine the packages listed as dependencies. These are the packages that compose the Temporal TypeScript SDK, and each package maps to the four parts of a Temporal application: an Activity, Client, Worker, and Workflow. There is also [Nanoid](https://npmjs.com/package/nanoid), an `npm` package which you'll use to generate a unique identifier for your Workflow. -Finally, look through the `devDependencies` section. These are the packages that let you set up a Node.js project with [Nodemon](https://www.npmjs.com/package/nodemon) (a tool that automatically restarts your application when it detects a change in your code), TypeScript, ESLint, and ts-node. +```json +"dependencies": { + "@temporalio/activity": "^1.11.5", + "@temporalio/client": "^1.11.5", + "@temporalio/worker": "^1.11.5", + "@temporalio/workflow": "^1.11.5", + "nanoid": "3.x" +}, +``` -Save the file, then download the dependencies specified in the `package.json` file with the command: +You'll use Node.js's build-in `fetch` library in your application. To use it with TypeScript, open the file `tsconfig.json` and locate the `"lib"` key under `"compilerOptions"`and add the `DOM` library to the array: -```command -npm install +```json +"compilerOptions": { + ... + "lib": ["es2020","DOM"], + ... +} ``` -Downloading the dependencies can take a few minutes to complete. Once the download is done, you will have new a `package-lock.json` file and a `node_modules` directory. +This ensures that `fetch` will be available as a global module. -Your project workspace is configured, so you're ready to create your first Temporal Activity and Workflow. You'll start with the Workflow. +With the project created, you'll create the application's core logic. -## Create a Workflow +## Write functions to call external services -Workflows are where you configure and organize the execution of Activities. You define a Workflow by writing a _Workflow Definition_ using one of the Temporal SDKs. You can learn more in the [Develop Workflows](https://docs.temporal.io/dev-guide/typescript/foundations/#develop-workflows) section of the Temporal documentation. +Your application will make two HTTP requests. The first request will return your current public IP, while the second request will use that IP to provide city, state, and country information. -To begin your Workflow, create a new subdirectory called `src`: +You'll use Temporal Activities to make these requests. You use Activities in your Temporal Applications to execute non-deterministic code or perform operations that may fail. -```command -mkdir src -``` +If an Activity fails, Temporal can automatically retry it until it succeeds or reaches a specified retry limit. This ensures that transient issues, like network glitches or temporary service outages, don't result in data loss or incomplete processes. -Create the file `workflows.ts` in the `src` directory. +Open `src/activities.ts` and remove all the existing code in the file. You'll replace it with your own. -:::info +Add the following code to define a Temporal Activity that retrieves your IP address from `icanhazip.com`: -Below, there are two language tabs (TypeScript and JavaScript). TypeScript is selected by default, and you will add that code to your file. The JavaScript code is an example of the code that the TypeScript compiler will generate, and you do not need to copy and paste it. This is true for all the following code samples. + +[src/activities.ts](https://github.com/temporalio/temporal-tutorial-ipgeo-ts/blob/main/src/activities.ts) +```ts +// Get the IP address +export async function getIP(): Promise { + const url = 'https://icanhazip.com'; + const response = await fetch(url); + const data = await response.text(); + return data.trim(); +} +``` + -::: +This function looks like a regular TypeScript function. With the Temporal TypeScript SDK, you define Activities using an exportable TypeScript module. -Next, add the following TypeScript code to define the Workflow: +The response from `icanhazip.com` is plain-text, and it includes a newline, so you trim off the newline character before returning the result. - -[hello-world/src/workflows.ts](https://github.com/temporalio/samples-typescript/blob/main/hello-world/src/workflows.ts) -```ts -import { proxyActivities } from '@temporalio/workflow'; -// Only import the activity types -import type * as activities from './activities'; +Notice that there's no error-handling code in this function. When you build your Workflow, you'll use Temporal's Activity Retry policies to retry this code automatically if there's an error. -const { greet } = proxyActivities({ - startToCloseTimeout: '1 minute', -}); +Now add the second Activity that accepts an IP address and retrieves location data. In `src/activities.ts`, add the following code to define it: -/** A workflow that simply calls an activity */ -export async function example(name: string): Promise { - return await greet(name); + +[src/activities.ts](https://github.com/temporalio/temporal-tutorial-ipgeo-ts/blob/main/src/activities.ts) +```ts +// Use the IP address to get the location. +export async function getLocationInfo(ip: string): Promise { + const url = `http://ip-api.com/json/${ip}`; + const response = await fetch(url); + const data = await response.json(); + return `${data.city}, ${data.regionName}, ${data.country}`; } ``` -In this code, the variable `greet` is assigned the value of `proxyActivites`, which is a method from the Temporal TypeScript SDK that lets you configure the Activity with different options. In this example, you have specified that the Start-to-Close Timeout for your Activity will be one minute, meaning that your Activity has one minute to begin before it times out. Of all the Temporal timeout options, `startToCloseTimeOut` is the one you should always set. +This Activity follows the same pattern as the `getIP` Activity. It's an exported async function that uses `fetch` to call a remote service. This time, the service returns JSON data rather than text. -The `example` function executes an Activity called `greet`, and the function returns the result of the Activity. You'll create the `greet` function in the next section. +While Activities can accept input arguments, it's a best practice to send a single argument rather than multiple arguments. In this case you only have a single String. If you have more than one argument, you should bundle them up in a serializable object. Review the [Activity parameters](https://docs.temporal.io/dev-guide/typescript/foundations/#activity-parameters) section of the Temporal documentation for more details, as there are some limitations you'll want to be aware of in more complex applications. -:::tip -No matter how many inputs you pass to a Workflow, it's a best practice to use an object. There are some JSON restrictions that you should be aware of, and you can learn more about the kinds of inputs you can pass to a Workflow in the [Workflow parameters](https://docs.temporal.io/dev-guide/typescript/foundations#workflow-parameters) section of the Temporal documentation. +You've created your two Activities. Now you'll coordinate them using a Temporal Workflow. -::: +## Control application logic with a Workflow -Next, you'll define the Activity that your Workflow will execute. +Workflows are where you configure and organize the execution of Activities. You define a Workflow by writing a *Workflow Definition* using one of the Temporal SDKs. Review the [Develop Workflows](https://docs.temporal.io/develop/typescript/core-application#develop-workflows) section of the Temporal documentation for more about Workflows in TypeScript. -## Create an Activity +Open the file `src/workflows.ts` and remove the code in the file since you'll add your own. Then add the following code to import the Activities and configure how the Workflow should handle failures with a [Retry Policy](https://docs.temporal.io/encyclopedia/retry-policies). -You use Activities in your Temporal Applications to execute non-deterministic code or perform operations that may fail. + +[src/workflows.ts](https://github.com/temporalio/temporal-tutorial-ipgeo-ts/blob/main/src/workflows.ts) +```ts +import * as workflow from '@temporalio/workflow'; -For this tutorial, your Activity won't be complex; you'll create an Activity that takes a string as input and uses it to create a new string as output, which is then returned to the Workflow. This will let you see how Workflows and Activities work together without building something complicated. +// Only import the activity types +import type * as activities from './activities'; -With the Temporal TypeScript SDK, you define Activities similarly to how you define Workflows: using an exportable TypeScript module. +// Load Activities and assign the Retry Policy +const { getIP, getLocationInfo} = workflow.proxyActivities({ + retry: { + initialInterval: '1 second', // amount of time that must elapse before the first retry occurs. + maximumInterval: '1 minute', // maximum interval between retries. + backoffCoefficient: 2, // how much the retry interval increases. + // maximumAttempts: 5, // maximum number of execution attempts. Unspecified means unlimited retries. + }, + startToCloseTimeout: '1 minute', // maximum time allowed for a single Activity Task Execution. +}); +``` + -Your Activity Definition can accept input parameters. Review the [Activity parameters](https://docs.temporal.io/dev-guide/typescript/foundations/#activity-parameters) section of the Temporal documentation for more details, as there are some limitations you'll want to be aware of when running more complex applications. +The Temporal TypeScript SDK requires that Workflows and Activities run in separate environments. Temporal Workflows [must be deterministic](https://docs.temporal.io/workflows#deterministic-constraints) so that Temporal can replay your Workflow in the event of a crash, and the TypeScript SDK runs Workflows in a sandbox that checks code for determinism to enforce this. -Create the file `activities.ts` in the `src` directory: +Since you run your non-deterministic operations in Activities, you configure your Workflow to call Activities through a proxy. That's why you import their types rather than the functions themselves. -Add the following code to define a `greet` function: +The `proxyActivities` method is also where you set options for how Temporal works with Activities. In this example, you have specified that the Start-to-Close Timeout for your Activity will be one minute, meaning that your Activity has one minute to complete before it times out. Of all the Temporal timeout options, `startToCloseTimeOut` is the one you should always set. - -[hello-world/src/activities.ts](https://github.com/temporalio/samples-typescript/blob/main/hello-world/src/activities.ts) +You also set the Retry Policy for Activities this way. In this example, you're using the default Retry Policy values, so you don't need to specify the values, but by leaving them in you have a more clear picture of what happens. Note that the `maximumAttempts` is commented out, which means there's no limit to the number of times Temporal will retry your Activities if they fail. + +With the imports and options in place, you can define the Workflow itself. In the TypeScript SDK, you implement a Workflow the same way you define an Activity; using an exportable async TypeScript function. Add the following code to call both Activities, using the value of the first as the input to the second: + + +[src/workflows.ts](https://github.com/temporalio/temporal-tutorial-ipgeo-ts/blob/main/src/workflows.ts) ```ts -export async function greet(name: string): Promise { - return `Hello, ${name}!`; +// The Temporal Workflow. +// Just a TypeScript function. +export async function getAddressFromIP(name: string): Promise { + + try { + const ip = await getIP(); + try { + const location = await getLocationInfo(ip); + return `Hello, ${name}. Your IP is ${ip} and your location is ${location}`; + } catch (e) { + throw new workflow.ApplicationFailure("Failed to get location"); + } + } catch (e) { + throw new workflow.ApplicationFailure("Failed to get IP"); + } + } ``` -You've completed the logic for the application; you have a Workflow and an Activity defined. Next, you'll write code to configure and launch a Worker. +This code uses a `try/catch` block, but because you've configured unlimited retries, there won't be any exceptions caught. However, if you change the Retry Policy's maximum retries, or you specify non-retryable exceptions, this code will be in place to handle those errors. + +Next you'll create a Worker that executes the Workflow and Activities. ## Configure and run a Worker -A [Worker](https://docs.temporal.io/concepts/what-is-a-worker) hosts Workflow and Activity functions and executes them one at a time. The Temporal Server tells the Worker to execute a specific function from information it pulls from the [Task Queue](https://docs.temporal.io/concepts/what-is-a-task-queue). After the Worker runs the code, it communicates the results back to the Temporal Server. +When you start a Temporal Workflow, the Workflow and its Activities get scheduled on the Temporal Service's [Task Queue](https://docs.temporal.io/concepts/what-is-a-task-queue). A [Worker](https://docs.temporal.io/concepts/what-is-a-worker) hosts Workflow and Activity functions and polls the Task Queue for tasks related to those Workflows and Activities. After the Worker runs the code, it communicates the results back to the Temporal Service where they're stored in the Event History. This records the Workflow's entire execution, enabling features like fault tolerance by allowing the Workflow to replay in case of Worker crashes or restarts. + +You use the Temporal SDK to define a Worker Program. -Create a file called `worker.ts` and add the following code to define the Worker: +In your Worker Program, you need to specify the name of the Task Queue, which must match the Task Queue name used whenever you interact with a Workflow from a client application. This ensures that the Worker processes tasks for the correct Workflow. The Task Queue name is a case-insensitive string. To ensure consistency and avoid errors, define the Task Queue name as a constant that can be reused throughout your code. - -[hello-world/src/worker.ts](https://github.com/temporalio/samples-typescript/blob/main/hello-world/src/worker.ts) +Open the file `temporal/src/shared.ts` and edit the following line to the file to define the constant for the Task Queue: + + +[src/shared.ts](https://github.com/temporalio/temporal-tutorial-ipgeo-ts/blob/main/src/shared.ts) +```ts +export const TASK_QUEUE_NAME="ip-address-ts"; +``` + + +When you created the project, the project generator created a Worker program for you. + +Open the file `src/worker.ts` in your editor and you'll see the following code: + + +[src/worker.ts](https://github.com/temporalio/temporal-tutorial-ipgeo-ts/blob/main/src/worker.ts) ```ts import { NativeConnection, Worker } from '@temporalio/worker'; import * as activities from './activities'; +import { TASK_QUEUE_NAME } from './shared'; async function run() { // Step 1: Establish a connection with Temporal server. @@ -234,25 +339,30 @@ async function run() { address: 'localhost:7233', // TLS and gRPC metadata configuration goes here. }); - // Step 2: Register Workflows and Activities with the Worker. - const worker = await Worker.create({ - connection, - namespace: 'default', - taskQueue: 'hello-world', - // Workflows are registered using a path as they run in a separate JS context. - workflowsPath: require.resolve('./workflows'), - activities, - }); + try { + // Step 2: Register Workflows and Activities with the Worker. + const worker = await Worker.create({ + connection, + namespace: 'default', + taskQueue: TASK_QUEUE_NAME, + // Workflows are registered using a path as they run in a separate JS context. + workflowsPath: require.resolve('./workflows'), + activities, + }); - // Step 3: Start accepting tasks on the `hello-world` queue - // - // The worker runs until it encounters an unexpected error or the process receives a shutdown signal registered on - // the SDK Runtime object. - // - // By default, worker logs are written via the Runtime logger to STDERR at INFO level. - // - // See https://typescript.temporal.io/api/classes/worker.Runtime#install to customize these defaults. - await worker.run(); + // Step 3: Start accepting tasks on the Task Queue specified in TASK_QUEUE_NAME + // + // The worker runs until it encounters an unexpected error or the process receives a shutdown signal registered on + // the SDK Runtime object. + // + // By default, worker logs are written via the Runtime logger to STDERR at INFO level. + // + // See https://typescript.temporal.io/api/classes/worker.Runtime#install to customize these defaults. + await worker.run(); + } finally { + // Close the connection once the worker has stopped + await connection.close(); + } } run().catch((err) => { @@ -262,288 +372,401 @@ run().catch((err) => { ``` -In the code, you create and call an async function named `run`. It creates and runs a Worker. It configures the Worker with a `workflowsPath` (the location of your workflow file), your Activity functions, and the name of the Task Queue. In this example, you name the Task Queue `hello-world`. +The code imports the `TASK_QUEUE_NAME` constant along with all the Activities in the `src/activities.ts` file. It then defines an async function named `run` that creates and runs a Worker that talks to the Temporal Service. The Worker takes a configuration object that specifies a connection to the Temporal Service, `workflowsPath` (the location of your Workflow file), your Activity functions, and the name of the Task Queue. + +In this case your Worker will run your Workflow and your two Activities, but there are cases where you could configure one Worker to run Activities, and another Worker to run the Workflows. -Now you will use an `npm` script to start your Worker using Nodemon, which will reload whenever it detects changes in your file, hence the command name `start.watch`. Run the command: +Now you will use an `npm` script to start your Worker with [Nodemon](https://nodemon.io/). Nodemon automatically reloads whenever it detects changes in your file, hence the command name `start.watch`. Be sure you have started the local Temporal Service and execute the following command to start your Worker: ```command npm run start.watch ``` -The script runs and produces output similar to the following: +The Worker runs and you see the following output: ```output -> temporal-hello-world@0.1.0 start -> ts-node src/worker.ts workflow.watch - -2023-11-22T04:38:12.091Z [INFO] Creating worker { +> empty@0.1.0 start.watch +> nodemon src/worker.ts + +[nodemon] 2.0.22 +[nodemon] to restart at any time, enter `rs` +[nodemon] watching path(s): src/**/* +[nodemon] watching extensions: ts +[nodemon] starting `ts-node src/worker.ts` +2024-12-09T18:27:38.879Z [INFO] Creating worker { + sdkComponent: 'worker', + taskQueue: 'ip-address-ts', options: { - namespace: 'default', - identity: '65411@User-MacBook-Pro.local', - shutdownGraceTime: '10s', - maxConcurrentActivityTaskExecutions: 100, - maxConcurrentLocalActivityExecutions: 100, - enableNonLocalActivities: true, - maxConcurrentWorkflowTaskExecutions: 100, - stickyQueueScheduleToStartTimeout: '10s', - maxHeartbeatThrottleInterval: '60s', - defaultHeartbeatThrottleInterval: '30s', - isolateExecutionTimeout: '5s', - workflowThreadPoolSize: 8, - maxCachedWorkflows: 261, - enableSDKTracing: false, - showStackTraceSources: false, - debugMode: false, - interceptors: { activityInbound: [Array], workflowModules: [Array] }, - sinks: { defaultWorkerLogger: [Object] }, - workflowsPath: '/Users/username/hello-world-temporal/src/workflows.ts', - activities: { greet: [AsyncFunction: greet] }, - taskQueue: 'hello-world', - shutdownGraceTimeMs: 10000, - stickyQueueScheduleToStartTimeoutMs: 10000, - isolateExecutionTimeoutMs: 5000, - maxHeartbeatThrottleIntervalMs: 60000, - defaultHeartbeatThrottleIntervalMs: 30000, - loadedDataConverter: { - payloadConverter: [DefaultPayloadConverter], - failureConverter: [DefaultFailureConverter], - payloadCodecs: [] - } + ... } } -2023-11-22T04:38:13.298Z [INFO] asset workflow-bundle-d624b57670e912aab581.js 695 KiB [emitted] [immutable] (name: main) -2023-11-22T04:38:13.298Z [INFO] runtime modules 937 bytes 4 modules -2023-11-22T04:38:13.298Z [INFO] modules by path ./node_modules/@temporalio/ 209 KiB -2023-11-22T04:38:13.298Z [INFO] modules by path ./node_modules/@temporalio/common/ 111 KiB 19 modules -2023-11-22T04:38:13.298Z [INFO] modules by path ./node_modules/@temporalio/workflow/ 95.2 KiB -2023-11-22T04:38:13.298Z [INFO] ./node_modules/@temporalio/workflow/lib/worker-interface.js 11.8 KiB [built] [code generated] -2023-11-22T04:38:13.299Z [INFO] ./node_modules/@temporalio/workflow/lib/alea.js 2.87 KiB [built] [code generated] -2023-11-22T04:38:13.299Z [INFO] + 11 modules -2023-11-22T04:38:13.299Z [INFO] ./node_modules/@temporalio/worker/lib/workflow-log-interceptor.js 2.25 KiB [built] [code generated] -2023-11-22T04:38:13.299Z [INFO] modules by path ./src/ 784 bytes -2023-11-22T04:38:13.299Z [INFO] ./src/workflows-autogenerated-entrypoint.js 540 bytes [built] [code generated] -2023-11-22T04:38:13.299Z [INFO] ./src/workflows.ts 244 bytes [built] [code generated] -2023-11-22T04:38:13.299Z [INFO] __temporal_custom_payload_converter (ignored) 15 bytes [built] [code generated] -2023-11-22T04:38:13.299Z [INFO] __temporal_custom_failure_converter (ignored) 15 bytes [built] [code generated] -2023-11-22T04:38:13.299Z [INFO] ./node_modules/ms/index.js 2.95 KiB [built] [code generated] -2023-11-22T04:38:13.299Z [INFO] webpack 5.75.0 compiled successfully in 540 ms -2023-11-22T04:38:13.303Z [INFO] Workflow bundle created { size: '0.68MB' } -2023-11-22T04:38:13.751Z [INFO] Worker state changed { state: 'RUNNING' } -``` - -In the output, you will see the Worker options and their values, the Webpack bundling output, and timestamps from Nodemon describing which programs have been invoked. - -Now that you have your Worker running, it's time for you to start a Workflow Execution. - -## Configure a Workflow Execution +... -You can start a Workflow Execution by using the Temporal CLI or by writing code using the Temporal SDK. +2024-12-09T18:27:39.412Z [INFO] Workflow bundle created { sdkComponent: 'worker', taskQueue: 'ip-address-ts', size: '0.82MB' } +2024-12-09T18:27:39.493Z [INFO] Worker state changed { + sdkComponent: 'worker', + taskQueue: 'ip-address-ts', + state: 'RUNNING' +} +``` -Starting a Workflow Execution using the Temporal SDK involves connecting to the Temporal Server, configuring the Task Queue the Workflow should use, and starting the Workflow with the input parameters it expects. In a real application, you may invoke this code when someone submits a form, presses a button, or visits a certain URL. In this tutorial, you will create a `client.ts` file that triggers your Temporal Workflow. +In the output, you see the Worker options and their values, the Webpack bundling output, and the state of the Worker. + +Your Worker is running and is polling the Temporal Service for Workflows to run, but before you start your Workflow, you'll write tests to prove it works as expected. -Open a new tab in your terminal, create a `client.ts` file in the `src` directory, and add the following code to your file to create a client that will kick off your Workflow Execution: +## Write tests to ensure things work - -[hello-world/src/client.ts](https://github.com/temporalio/samples-typescript/blob/main/hello-world/src/client.ts) +The Temporal TypeScript SDK includes functions that help you test your Workflow executions. Let's add a basic unit test to the application to make sure the Workflow works as expected. + +You'll use the [mocha](https://mochajs.org/) package to build your test cases and mock the Activity so you can test the Workflow in isolation. You'll also use the `@temporalio/testing` package, which will download a test server that provides a `TestWorkflowEnvironment`. + +Create the file `src/mocha/workflows-mocks.test.ts` and add the following code to set up the testing environment: + + +[src/mocha/workflows-mocks.test.ts](https://github.com/temporalio/temporal-tutorial-ipgeo-ts/blob/main/src/mocha/workflows-mocks.test.ts) ```ts -import { Connection, Client } from '@temporalio/client'; -import { example } from './workflows'; -import { nanoid } from 'nanoid'; +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { after, before, it } from 'mocha'; +import { Worker } from '@temporalio/worker'; +import { getAddressFromIP } from '../workflows'; +import assert from 'assert'; +``` + -async function run() { - // Connect to the default Server location - const connection = await Connection.connect({ address: 'localhost:7233' }); - // In production, pass options to configure TLS and other settings: - // { - // address: 'foo.bar.tmprl.cloud', - // tls: {} - // } - - const client = new Client({ - connection, - // namespace: 'foo.bar', // connects to 'default' namespace if not specified +`TestWorkflowEnvironment` is a runtime environment used to test a Workflow. You use it to connect the Client and Worker to the test server and interact with the test server. You'll use this to register your Workflow Type and access information about the Workflow Execution, such as whether it completed successfully and the result or error it returned. Since the `TestWorkflowEnvironment` will be shared across tests, you will set it up before all of your tests, and tear it down after your tests finish. + +Add the following code to configure the testing environment and test the Workflow execution: + + +[src/mocha/workflows-mocks.test.ts](https://github.com/temporalio/temporal-tutorial-ipgeo-ts/blob/main/src/mocha/workflows-mocks.test.ts) +```ts +describe('getAddressFromIP', () => { + let testEnv: TestWorkflowEnvironment; + + before(async () => { + testEnv = await TestWorkflowEnvironment.createLocal(); }); - const handle = await client.workflow.start(example, { - taskQueue: 'hello-world', - // type inference works! args: [name: string] - args: ['Temporal'], - // in practice, use a meaningful business ID, like customerId or transactionId - workflowId: 'workflow-' + nanoid(), + after(async () => { + await testEnv?.teardown(); }); - console.log(`Started workflow ${handle.workflowId}`); - // optional: wait for client result - console.log(await handle.result()); // Hello, Temporal! -} + it('successfully completes the Workflow with mocked Activities', async () => { + const { client, nativeConnection } = testEnv; + const taskQueue = 'test'; -run().catch((err) => { - console.error(err); - process.exit(1); + const worker = await Worker.create({ + connection: nativeConnection, + taskQueue, + workflowsPath: require.resolve('../workflows'), + activities: { + getIP: async (): Promise => '1.1.1.1', + getLocationInfo: async (_ip: string): Promise => "Planet Earth" + }, + }); + + const result = await worker.runUntil( + client.workflow.execute(getAddressFromIP, { + args: ['Temporal'], + workflowId: 'test', + taskQueue, + }) + ); + assert.equal(result, "Hello, Temporal. Your IP is 1.1.1.1 and your location is Planet Earth"); + }); }); ``` -In the `client.ts` file, the `run` function sets up a connection to your Temporal Server, invokes your Workflow, passes in an argument for the `name` parameter (in this example, the name is Temporal) and assigns the Workflow a unique identifier with Nanoid. The client dispatches the Workflow on the same `hello-world` Task Queue that the Worker is polling on. +This test sets up a test environment to run Workflows that uses a lightweight Temporal Service specifically for testing. In the test itself, you create a Worker that connects to the test environment. This should look familiar, as it's similar to the code you wrote to define your Worker Program. -:::tip Specify a Workflow Id -A Workflow Id is unique in a namespace and is used for deduplication. Using an identifier that reflects some business process or entity is a good practice. For example, you might use a customer identifier as part of the Workflow Id if you run one Workflow per customer. This would make it easier to find all of the Workflow Executions related to that customer later. -::: +Instead of using your actual Activities, you replace the Activities `getIP` and `getAddress` with async functions that return hard-coded values. This way you're testing the Workflow's logic independently of the Activities. If you wanted to test the Activities directly as part of an integration test, you'd specify them directly as you did when you wrote the Worker program. -:::tip Get your results now or later - -You can [get the results](https://docs.temporal.io/dev-guide/typescript/foundations/#get-workflow-results) from your Workflow right away, or you can get the results at a later time. This implementation attempts to get the results immediately by logging the output of the `example` Workflow which in turn calls the `greet` Activity. -::: +Running the tests requires using the `mocha` command along with requiring the following libraries and pointing the test runner to the appropriate folder. Here's what the command would look like to run the tests: -Now that your client code is written, it's time for you to use this code to start your Workflow Execution. +```command +npx mocha \ + --require ts-node/register \ + --require source-map-support/register \ + src/mocha/*.test.ts +``` -## Start a Workflow Execution +However, since this is a lot to type into the command line every time, you'll find a `test` script defined in `package.json` that runs this command for you. -To start your Workflow, open a new tab in your terminal and run the `npm` script: +Ensure you've saved all your files and execute your tests with the following command: ```command -npm run workflow +npm test ``` -The script runs and prints the result: +The test environment starts, spins up a Worker, and executes the Workflow in the test environment. At the end, you'll see that your test passes: -``` -Hello, Temporal! -``` +```output + ✓ successfully completes the Workflow with mocked Activities (543ms) -The "Hello, Temporal!" output is generated as the result of a few steps. First, `client.ts` passes `Temporal` as an argument to the Workflow. Next, the Workflow passes the argument to the Activity, and finally the Activity returns `Hello, ${name}!`. + 1 passing (723ms) +``` -You have successfully built a Temporal application from scratch! +:::tip +If you get an error that your test has timed out, run it again. The first time you run the tests on your local machine, the test server might not start quickly enough. +::: -## ![](/img/icons/check.png) Add a unit test +With a Workflow test in place, you can write unit tests for the Activities. -The Temporal TypeScript SDK includes functions that help you test your Workflow executions. Let's add a basic unit test to the application to make sure the Workflow works as expected. +Both of your Activities make external calls to services that will change their results based on who runs them. It will be challenging to test these Activities reliably. For example, the IP address may vary based on your machine's location. -You'll use the [mocha](https://mochajs.org/) package to build your test cases and mock the Activity so you can test the Workflow in isolation. You'll also use the `@temporalio/testing` package which will download a test server which provides a `TestWorkflowEnvironment`. +To ensure you can test the Activities in isolation, you’ll stub out the `fetch` calls using the [`sinon`](https://sinonjs.org/) library. -`TestWorkflowEnvironment` is a runtime environment used to test a Workflow. It is used to connect the Client and Worker to the test server and interact with the test server. You'll use this to register your Workflow Type and access information about the Workflow Execution, such as whether it completed successfully and the result or error it returned. Since the `TestWorkflowEnvironment` will be shared across tests, you will set it up before all of your -tests, and tear it down after your tests are done. +Sinon.js is a JavaScript testing library that provides powerful tools like spies, stubs, and mocks, allowing you to replace dependencies with controlled, testable behavior. -When running your Workflow code within the test environment, some aspects -of its execution will work differently to better support testing. For -example, Timers will "skip time" by firing without their normal delay, -enabling you to quickly test long-running Workflows. +Add `sinon` as a development dependency, along with its type definitions: -`TestWorkflowEnvironment.createTimeSkipping` starts a time skipping workflow environment. Alternatively, as you will see in the example below, you can use the `TestWorkflowEnvironment.createLocal` method to start a workflow environment that includes full server capabilities but no support for time skipping. +```command +npm i sinon @types/sinon --save-dev +``` -To set up your tests, add `mocha` and `@temporalio/testing` to your `package.json` file along with the types as development dependencies: +Create the file `src/mocha/activities.test.ts` and add the following code to import the testing libraries you'll use: -```typescript -"devDependencies": { - "@temporalio/testing": "~1.9.0", - "@types/mocha": "8.x", - "mocha": "8.x" -} + +[src/mocha/activities.test.ts](https://github.com/temporalio/temporal-tutorial-ipgeo-ts/blob/main/src/mocha/activities.test.ts) +```ts +import { MockActivityEnvironment } from '@temporalio/testing'; +import { describe, it } from 'mocha'; +import * as activities from '../activities'; +import assert from 'assert'; +import sinon from 'sinon'; ``` + -Running the tests requires using the `mocha` command along with requiring the following libraries and pointing the test runner to the appropriate folder. Here's what the command would look like to run the tests: -```command -npx mocha \ - --require ts-node/register \ - --require source-map-support/register \ - src/mocha/*.test.ts +The `MockActivityEnvironment` from the `@temporalio/testing` package lets you test Activities as if they were part of a Temporal Application. + +Next, write the test for the `getIP` Activity, using `sinon` to stub out actual HTTP calls so your tests are consistent. Notice that the stubbed response adds the newline character so it replicates the actual response: + + +[src/mocha/activities.test.ts](https://github.com/temporalio/temporal-tutorial-ipgeo-ts/blob/main/src/mocha/activities.test.ts) +```ts +describe('ip activity', async () => { + it('successfully gets the ip', async () => { + const fakeIP = '123.45.67.89'; + const stub = sinon.stub(global, 'fetch').resolves({ + text: () => Promise.resolve(`${fakeIP}\n`), + } as Response); + + try { + const env = new MockActivityEnvironment(); + const ip = await env.run(activities.getIP); + assert.strictEqual(ip, fakeIP); + } finally { + stub.restore(); + } + }); +}); ``` + -However, since this is quite verbose to type into the command line every time, we recommend that you add this command into the scripts of your `package.json` file like so: +To test the Activity itself, you use the `MockActivityEnvironment` to execute the Activity rather than directly calling the `getIP` function. -```typescript -"scripts": { - ... - "test": "mocha --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" - ... -}, +The `try/finally` block ensures that if the test fails, the tests restore the `fetch` stub to its original functionality. +This way other tests you write can also stub `fetch` with a different response. + +To test the `getLocation` Activity, you use a similar approach. Add the following code to the `src/mocha/activities.ts` file: + + +[src/mocha/activities.test.ts](https://github.com/temporalio/temporal-tutorial-ipgeo-ts/blob/main/src/mocha/activities.test.ts) +```ts +describe('getLocation activity', async () => { + it('successfully gets the location', async () => { + const ip = '123.45.67.89'; + const fakeLocation = { + city: 'Sample City', + regionName: 'Sample Region', + country: 'Sample Country' + }; + + const stub = sinon.stub(global, 'fetch').resolves({ + json: () => Promise.resolve(fakeLocation), + } as Response); + + try { + const env = new MockActivityEnvironment(); + const location = await env.run(activities.getLocationInfo, ip); + assert.strictEqual(location, `${fakeLocation.city}, ${fakeLocation.regionName}, ${fakeLocation.country}`); + } finally { + stub.restore(); + } + }); +}); ``` + + +This test looks similar to the previous test; you stub out the `fetch` method and ensure it returns the expected data, and then you execute the Actiity in the `MockActivityEnvironment`. Then you restore the stubbed `fetch` method. -Save the file and execute your tests with the script: +Run the tests again to see them pass: ```command npm test ``` -The following code provides an example of how you could test this Workflow. In order to do this, -you need to import the `TestWorkflowEnvironment`, the pieces from `Mocha` that you need, as well as -your Worker, Workflow, and the Activities. +Now that you have your tests passing, it's time to start a Workflow Execution. -Create the file `workflows.test.ts` and add the following code to the file to define the Workflow test: +## Run the Workflow from a client - -[hello-world/src/mocha/workflows.test.ts](https://github.com/temporalio/samples-typescript/blob/main/hello-world/src/mocha/workflows.test.ts) -```ts -import { TestWorkflowEnvironment } from '@temporalio/testing'; -import { before, describe, it } from 'mocha'; -import { Worker } from '@temporalio/worker'; -import { example } from '../workflows'; -import * as activities from '../activities'; -import assert from 'assert'; +You can start a Workflow Execution by using the Temporal CLI or by writing code using the Temporal SDK. -describe('Example workflow', () => { - let testEnv: TestWorkflowEnvironment; +Starting a Workflow Execution using the Temporal SDK involves connecting to the Temporal Server, configuring the Task Queue the Workflow should use, and starting the Workflow with the input parameters it expects. In a real application, you may invoke this code when someone submits a form, presses a button, or visits a certain URL. In this tutorial, you will create a `client.ts` file that triggers your Temporal Workflow. - before(async () => { - testEnv = await TestWorkflowEnvironment.createLocal(); - }); +The project generator created a `client.ts` file in the `src` directory, but it will need a couple changes to make it work for this application: - after(async () => { - await testEnv?.teardown(); - }); +- You'll need to change the name of the Workflow. +- You'll need to add code that lets you specify the person's name as the command-line argument so you can pass it to the Workflow, instead of using the hard-coded value of `Temporal`. - it('successfully completes the Workflow', async () => { - const { client, nativeConnection } = testEnv; - const taskQueue = 'test'; +Open the `src/client.ts` file in your editor. Remove its contents and replace it with the following code: - const worker = await Worker.create({ - connection: nativeConnection, - taskQueue, - workflowsPath: require.resolve('../workflows'), - activities, - }); + +[src/client.ts](https://github.com/temporalio/temporal-tutorial-ipgeo-ts/blob/main/src/client.ts) +```ts +import { Connection, Client } from '@temporalio/client'; +import { getAddressFromIP } from './workflows'; +import { nanoid } from 'nanoid'; +import { TASK_QUEUE_NAME } from './shared'; +import process from 'process'; - const result = await worker.runUntil( - client.workflow.execute(example, { - args: ['Temporal'], - workflowId: 'test', - taskQueue, - }) - ); - assert.equal(result, 'Hello, Temporal!'); +async function run() { + + if (process.argv.length <= 2) { + console.error('Must specify a name as the command-line argument'); + process.exit(1); + } + + const name = process.argv[2]; + const connection = await Connection.connect({ address: 'localhost:7233' }); + const client = new Client({ connection }); + + const handle = await client.workflow.start(getAddressFromIP, { + taskQueue: TASK_QUEUE_NAME, + args: [name], + workflowId: 'getAddressFromIP-' + nanoid(), }); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); }); ``` -This test creates a test execution environment and then registers the Activity to the Worker. The test then executes the Workflow in the test environment and checks for a successful execution. Finally, the test ensures the Workflow's return value returns the expected value. +You import the `process` module so you can access command-line arguments you'll pass. + +In the `run` function you check to see if there are at least two arguments passed. You need to check for two arguments because `process.argv` includes the Node.js executable path and the script path as the first two elements, so the actual command-line arguments start at index 2. -Run the following command from the project root to execute the unit tests: +The `run` function then sets up a connection to your Temporal Server, invokes your Workflow, passes in an argument for the `name`, and assigns the Workflow a unique identifier with Nanoid. The client dispatches the Workflow on the same Task Queue that the Worker is polling on. That's why you used a constant to ensure the Task Queue name is consistent. If there's a mismatch, your Workflow will execute on a different Task Queue and there won't be any Workers polling for tasks. + +:::note Specify a Workflow Id + +A Workflow ID is unique in a Namespace and identifies a Workflow Execution. Using an identifier that reflects some business process or entity is a good practice. For example, you might use a customer identifier as part of the Workflow Id if you run one Workflow per customer. This would make it easier to find all Workflow Executions related to that customer later. + +In this tutorial you're generating a UUID and appending it to a string that identifies the Workflow. +::: + +Now you can run your Workflow. First, ensure that your local Temporal Service is running, and that your Worker program is running also. + +Then open a new terminal and switch to the project directory: ```command -npm test +cd temporal-ip-geolocation +``` + +:::tip + +To run your Temporal Application, you need to start the Workflow and the Worker. You can start these in any order, but you'll need to run each command from a separate terminal window, as the Worker needs to be constantly running to look for tasks to execute. + +::: + +Now run the following command to run the Workflow using the client program you wrote, using your name as the argument: + +```command +npm run workflow Brian ``` -You'll see output similar to the following from your test run indicating that the test was successful: +You'll see the following output: +```output +> temporal-hello-world@0.1.0 workflow +> ts-node src/client.ts Brian + +Hello, Brian. Your IP is 198.51.100.23 and your location is Seattle, Washington, United States ``` -> temporal-hello-world@0.1.0 test -> mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts - greet activity - ✓ successfully greets the user +Your Temporal Application works. Now review it in the Temporal Web UI. + +## Exploring your application in the Web UI + +The Temporal Web UI gives you insights into your Workflow's execution. Open the Temporal Web UI by visiting `http://localhost:8233` and click on your completed Workflow to view the execution history. You'll see results similar to the following image: + +![The UI shows the results of the Workflow including the output](images/overview.png) + +You'll see the dates the Workflow Execution ran, how long it took to run, the input to the Workflow, and the result. + +After that, you see the Event History, detailing the entire flow, including the inputs and outputs of the Activity Executions: + +![The Workflow History showing all Activities and their results, oldest to newest.](images/history.png) + +The most recent event is at the top, so read the history from the bottom up to see each step in the process. Using this history, you can see exactly how your Workflow executed and pinpoint any places things might have gone wrong. + +Temporal stores the results of each Activity in this history, as you can see in the image. If there was a system crash between the `getIP` and `getLocationInfo` Activity Executions, a new Worker would re-run the Workflow, but would use the previous Event History to reconstruct the Workflow's state. Instead of re-running the `getIP` function, it would use the previous run's value and continue on. This prevents duplicate executions. By relying on the stored Event History, Temporal ensures that the Workflow can recover seamlessly, maintaining reliability and consistency even after a crash. + +In this application, this recovery isn't crucial. But imagine a situation where each Activity execution was a bank transaction. If a crash occurred between transactions, the Worker can pick up where the previous one failed. Nobody gets charged multiple times because something failed. + +Next, you'll explore how Temporal handles failed Activities. + +## Observe automatic retries + +When you developed your Activities, you didn't include any error-handling code. So if there's a problem making the request, the Workflow will handle the error using the Retry Policy. + +Test this out. Disconnect your local machine from the Internet by turning off your Wi-Fi connection or unplugging your network cable. + +Then, with the local Temporal Service running and your Worker running, switch to the Terminal window where you ran your Workflow and run it again: + +```command +npm run workflow ``` -You have a working application and a test to ensure the Workflow executes as expected. +This time you don't get a response. + +Visit `http://localhost:8233` to open the Temporal Web UI and locate the Workflow Execution that's currently running. When you select it, you'll see something like the following image, indicating that there's a problem: +![The timeline shows the Activity failure](images/timeline_failed.png) + +As you can see, the `getIP` Activity has failed and Temporal is retrying it. Scroll down to the Event History and you'll see the failure represented there: + +![The Event History shows the failed Activity](images/history_failed.png) +Select the **Pending Activity** item in the table to see why it failed and you'll see the stack trace: + +![The Activity stack trace shows the error](images/activity_stack_trace.png) + +In the **Last Failure** field, you can see there was a TypeScript error indicating that `fetch` failed. + +Connect to the internet again and wait. After a few moments, the Workflow recovers and completes: + +![Once the network comes back the history updates and shows that things completed.](images/history_recovered.png) + +If you return to your terminal where you launched the Workflow, you'll find your results there as well. + +You can recover from failures by letting Temporal handle them for you instead of writing complex error-handling logic. You can also decide that you only want to retry a fixed number of times, or that you only want to recover on certain kinds of errors. ## Conclusion -You now know how to build a basic Temporal application using the TypeScript SDK. +In this tutorial you built your first Temporal Application, using TypeScript functions to build a resilient application that recovered from failure. You wrote tests to verify that it works and reviewed the Event History for a working execution. You also tested your Workflow without an internet connection to understand how Temporal recovers from failures like network outages. + +Take this application one step further and add a new Activity that gets the current weather for the location you found. + +Then [get your application working on Temporal Cloud](https://learn.temporal.io/getting_started/typescript/run_workers_with_cloud_typescript/). ### Review -Answer the following questions to see whether you remember some of the important concepts from this tutorial: +Answer the following questions to see whether you remember some important concepts from this tutorial:
diff --git a/snipsync.config.yaml b/snipsync.config.yaml index a9559e52..dd98969a 100644 --- a/snipsync.config.yaml +++ b/snipsync.config.yaml @@ -1,4 +1,6 @@ origins: + - owner: temporalio + repo: temporal-tutorial-ipgeo-ts - owner: temporalio repo: money-transfer-project-template-dotnet - owner: temporalio