Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .scripts/copy-shared-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ testProjectsNode.value.items = [];

for (const sample of samples) {
// Don't use require, because it won't work with ESM samples
const packageJson = JSON.parse(readFileSync(`../${sample}/package.json`));
const packageJson = JSON.parse(readFileSync(`./${sample}/package.json`));
const hasTestScript = !!packageJson.scripts.test;

if (hasTestScript) {
Expand Down
1 change: 1 addition & 0 deletions .scripts/list-of-samples.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"mutex",
"nestjs-exchange-rates",
"nextjs-ecommerce-oneclick",
"nexus-hello",
"patching-api",
"polling",
"production",
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ and you'll be given the list of sample options.
- [**Dependency Injection**](./activities-dependency-injection): Share dependencies between activities (for example, when you need to initialize a database connection once and then pass it to multiple activities).
- [**Worker-Specific Task Queues**](./worker-specific-task-queues): Use a unique task queue per Worker to have certain Activities only run on that specific Worker. For instance for a file processing Workflow, where the first Activity is downloading a file, and subsequent Activities need to operate on that file. (If multiple Workers were on the same queue, subsequent Activities may get run on different machines that don't have the downloaded file.)

#### Nexus APIs

- [**Nexus Hello**](./nexus-hello): Demonstrates how to define a Nexus Service, implement the Operation handlers, and call the Operations from a Workflow.

#### Workflow APIs

- **Timers**:
Expand Down
3 changes: 3 additions & 0 deletions nexus-hello/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
lib
.eslintrc.js
48 changes: 48 additions & 0 deletions nexus-hello/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const { builtinModules } = require('module');

const ALLOWED_NODE_BUILTINS = new Set(['assert']);

module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint', 'deprecation'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
rules: {
// recommended for safety
'@typescript-eslint/no-floating-promises': 'error', // forgetting to await Activities and Workflow APIs is bad
'deprecation/deprecation': 'warn',

// code style preference
'object-shorthand': ['error', 'always'],

// relaxed rules, for convenience
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'@typescript-eslint/no-explicit-any': 'off',
},
overrides: [
{
files: ['src/workflows.ts', 'src/workflows-*.ts', 'src/workflows/*.ts'],
rules: {
'no-restricted-imports': [
'error',
...builtinModules.filter((m) => !ALLOWED_NODE_BUILTINS.has(m)).flatMap((m) => [m, `node:${m}`]),
],
},
},
],
};
2 changes: 2 additions & 0 deletions nexus-hello/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
lib
node_modules
1 change: 1 addition & 0 deletions nexus-hello/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
1 change: 1 addition & 0 deletions nexus-hello/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
18 changes: 18 additions & 0 deletions nexus-hello/.post-create
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
To begin development, install the Temporal CLI:

Mac: {cyan brew install temporal}
Other: Download and extract the latest release from https://github.com/temporalio/cli/releases/latest

Start Temporal Server:

{cyan temporal server start-dev}

Use Node version 18+ (v22.x is recommended):

Mac: {cyan brew install node@22}
Other: https://nodejs.org/en/download/

Then, in the project directory, using two other shells, run these commands:

{cyan npm run start.watch}
{cyan npm run workflow}
1 change: 1 addition & 0 deletions nexus-hello/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lib
2 changes: 2 additions & 0 deletions nexus-hello/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
printWidth: 120
singleQuote: true
78 changes: 78 additions & 0 deletions nexus-hello/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Temporal+Nexus RPC Hello World

This sample demonstrates:

- How to define a Nexus Service;
- How to call a Nexus Operation from a Workflow;
- How to implement a Nexus Operation handler using the Synchronous handler form;
- How to implement a Nexus Operation handler that starts a Temporal Workflow;
- How to use Nexus to make calls across Temporal namespaces.

## Structure

- `src/api.ts` - Defines the Nexus Service, including its input and output types.
- `src/caller/` - Sample Workflows that call the Nexus Operations.
- `src/service/` - The Nexus Service handler, together with a Workflow used by one of the Nexus Operations.
- `src/starter.ts` - Starter code, to run the Workflow.

## Prerequisites

Instructions below assume the following:

- [Install the latest Temporal CLI](https://learn.temporal.io/getting_started/typescript/dev_environment/#set-up-a-local-temporal-service-for-development-with-temporal-cli) (`v1.4.1` or higher recommended)
- [Install the latest Temporal TypeScript SDK](https://learn.temporal.io/getting_started/typescript/dev_environment/#add-temporal-typescript-sdk-dependencies) (`v1.13.0` or higher)

:::tip SUPPORT, STABILITY, and DEPENDENCY INFO

Temporal TypeScript SDK support for Nexus is at [Pre-release](https://docs.temporal.io/evaluate/development-production-features/release-stages#pre-release).

All APIs are experimental and may be subject to backwards-incompatible changes.

:::

## Running the sample

### Preparation

1. Install NPM dependencies:

```sh
npm install # or `pnpm` or `yarn`
```

2. Make sure you have a local [Temporal Server](https://github.com/temporalio/cli/#installation) running:

```sh
temporal server start-dev --port 7233
```

3. Create the expected namespaces:

```bash
temporal operator namespace create --namespace my-caller-namespace
temporal operator namespace create --namespace my-target-namespace
```

4. Setup the Nexus Endpoint on the caller namespace:

```bash
temporal operator nexus endpoint create \
--name my-nexus-endpoint-name \
--target-namespace my-target-namespace \
--target-task-queue my-handler-task-queue
```

### Execution

1. Run `npm run start.service` to start the Worker that will be serving the Nexus Operation handlers and its associated Workflows. That Worker connects to the `my-target-namespace` namespace.

2. In another shell, run `npm run start.caller` to start the Worker that will be serving the Caller Workflows. That Worker connects to the `my-caller-namespace` namespace.

3. In a third shell, `npm run workflow` to start an instance of the caller Workflows.

Example output:

```bash
Echo message: This message is from the client
Hello message: Hello, Temporal!
```
49 changes: 49 additions & 0 deletions nexus-hello/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "nexus-hello",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "tsc --build",
"build.watch": "tsc --build --watch",
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "eslint .",
"start.caller": "ts-node src/caller/worker.ts",
"start.service": "ts-node src/service/worker.ts",
"workflow": "ts-node src/starter.ts"
},
"nodemonConfig": {
"execMap": {
"ts": "ts-node"
},
"ext": "ts",
"watch": [
"src"
]
},
"dependencies": {
"@temporalio/activity": "^1.13.0",
"@temporalio/client": "^1.13.0",
"@temporalio/nexus": "^1.13.0",
"@temporalio/worker": "^1.13.0",
"@temporalio/workflow": "^1.13.0",
"nexus-rpc": "^0.0.1",
"nanoid": "3.x"
},
"devDependencies": {
"@temporalio/testing": "^1.13.0",
"@tsconfig/node18": "^18.2.4",
"@types/mocha": "8.x",
"@types/node": "^22.9.1",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-deprecation": "^3.0.0",
"mocha": "8.x",
"nodemon": "^3.1.7",
"prettier": "^3.4.2",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
}
}
36 changes: 36 additions & 0 deletions nexus-hello/src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// @@@SNIPSTART typescript-nexus-hello-service
import * as nexus from 'nexus-rpc';

export const helloService = nexus.service('hello', {
/**
* Return the input message, unmodified. In the present sample, this Operation
* will be implemented using the Synchronous Nexus Operation handler syntax.
*/
echo: nexus.operation<EchoInput, EchoOutput>(),

/**
* Return a salutation message, in the requested language. In the present sample,
* this Operation will be implemented by starting the `helloWorkflow` Workflow.
*/
hello: nexus.operation<HelloInput, HelloOutput>(),
});

export interface EchoInput {
message: string;
}

export interface EchoOutput {
message: string;
}

export interface HelloInput {
name: string;
language: LanguageCode;
}

export interface HelloOutput {
message: string;
}

export type LanguageCode = 'en' | 'fr' | 'de' | 'es' | 'tr';
// @@@SNIPEND
26 changes: 26 additions & 0 deletions nexus-hello/src/caller/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NativeConnection, Worker } from '@temporalio/worker';

async function run() {
const connection = await NativeConnection.connect({
address: 'localhost:7233',
});
try {
const namespace = 'my-caller-namespace';
const callerTaskQueue = 'nexus-hello-caller-task-queue';
const worker = await Worker.create({
connection,
namespace,
taskQueue: callerTaskQueue,
workflowsPath: require.resolve('./workflows'),
});

await worker.run();
} finally {
await connection.close();
}
}

run().catch((err) => {
console.error(err);
process.exit(1);
});
32 changes: 32 additions & 0 deletions nexus-hello/src/caller/workflows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// @@@SNIPSTART typescript-nexus-hello-caller-workflow
import * as wf from '@temporalio/workflow';
import { helloService, LanguageCode } from '../api';

const HELLO_SERVICE_ENDPOINT = 'my-nexus-endpoint-name';

export async function echoCallerWorkflow(message: string): Promise<string> {
const nexusClient = wf.createNexusClient({
service: helloService,
endpoint: HELLO_SERVICE_ENDPOINT,
});

const result = await nexusClient.executeOperation('echo', { message }, { scheduleToCloseTimeout: '10s' });

return result.message;
}

export async function helloCallerWorkflow(name: string, language: LanguageCode): Promise<string> {
const nexusClient = wf.createNexusClient({
service: helloService,
endpoint: HELLO_SERVICE_ENDPOINT,
});

const helloResult = await nexusClient.executeOperation(
'hello',
{ name, language },
{ scheduleToCloseTimeout: '10s' },
);

return helloResult.message;
}
// @@@SNIPEND
39 changes: 39 additions & 0 deletions nexus-hello/src/service/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// @@@SNIPSTART typescript-nexus-hello-service-handler
import { randomUUID } from 'crypto';
import * as nexus from 'nexus-rpc';
import * as temporalNexus from '@temporalio/nexus';
import { helloService, EchoInput, EchoOutput, HelloInput, HelloOutput } from '../api';
import { helloWorkflow } from './workflows';

export const helloServiceHandler = nexus.serviceHandler(helloService, {
echo: async (ctx, input: EchoInput): Promise<EchoOutput> => {
// A simple async function can be used to defined a Synchronous Nexus Operation.
// This is often sufficient for Operations that simply make arbitrary short calls to
// other services or databases, or that perform simple computations such as this one.
//
// You may also access a Temporal Client by calling `temporalNexus.getClient()`.
// That Client can be used to make arbitrary calls, such as signaling, querying,
// or listing workflows.
return input;
},
hello: new temporalNexus.WorkflowRunOperationHandler<HelloInput, HelloOutput>(
// WorkflowRunOperationHandler takes a function that receives the Operation's context and input.
// That function can be used to validate and/or transform the input before passing it to
// the Workflow, as well as to customize various Workflow start options as appropriate.
// Call temporalNexus.startWorkflow() to actually start the Workflow from inside the
// WorkflowRunOperationHandler's delegate function.
async (ctx, input: HelloInput) => {
return await temporalNexus.startWorkflow(ctx, helloWorkflow, {
args: [input],

// Workflow IDs should typically be business-meaningful IDs and are used to dedupe workflow starts.
// For this example, we're using the request ID allocated by Temporal when the caller workflow schedules
// the operation, this ID is guaranteed to be stable across retries of this operation.
workflowId: ctx.requestId ?? randomUUID(),

// Task queue defaults to the task queue this Operation is handled on.
});
},
),
});
// @@@SNIPEND
Loading
Loading