Skip to content
Open
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
3 changes: 3 additions & 0 deletions .scripts/copy-shared-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const TSCONFIG_EXCLUDE = [
'empty',
'hello-world',
'scratchpad',
'langsmith',
];
const GITIGNORE_EXCLUDE = [
'nextjs-ecommerce-oneclick',
Expand All @@ -48,6 +49,7 @@ const ESLINTRC_EXCLUDE = [
'protobufs',
'food-delivery',
'nestjs-exchange-rates',
'langsmith',
];
const ESLINTIGNORE_EXCLUDE = [
'production',
Expand Down Expand Up @@ -89,6 +91,7 @@ const POST_CREATE_EXCLUDE = [
'empty',
'scratchpad',
'workflow-streams',
'langsmith',
];

const PRETTIERRC_EXCLUDE = ['food-delivery'];
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 @@ -33,6 +33,7 @@
"nexus-cancellation",
"nexus-hello",
"nexus-messaging",
"nexus-standalone-activity",
"nexus-standalone-operations",
"openai-agents",
"patching-api",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ and you'll be given the list of sample options.
- [**Nexus Hello**](./nexus-hello): Demonstrates how to define a Nexus Service, implement the Operation handlers, and call the Operations from a Workflow.
- [**Nexus Cancellation**](./nexus-cancellation): Demonstrates how to cancel a Nexus Operation from a caller workflow using a CancellationScope
- [**Nexus Standalone Operations**](./nexus-standalone-operations): Execute Nexus Operations directly from a Temporal Client, without a caller Workflow.
- [**Nexus Standalone Activity**](./nexus-standalone-activity): Use a `TemporalOperationHandler` to execute a Nexus Operation as a standalone Activity.

#### Workflow APIs

Expand Down
3 changes: 3 additions & 0 deletions nexus-standalone-activity/.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-standalone-activity/.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-standalone-activity/.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-standalone-activity/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
1 change: 1 addition & 0 deletions nexus-standalone-activity/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
18 changes: 18 additions & 0 deletions nexus-standalone-activity/.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-standalone-activity/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lib
2 changes: 2 additions & 0 deletions nexus-standalone-activity/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
printWidth: 120
singleQuote: true
59 changes: 59 additions & 0 deletions nexus-standalone-activity/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Nexus Operation backed by a standalone Activity

This sample demonstrates how a Nexus Operation implemented with a `TemporalOperationHandler` can start a standalone Activity. The starter invokes the Nexus Operation directly from a Temporal Client, the handler starts the Activity, and the Activity result completes the Operation.

These APIs are experimental and may change in future releases.

## Structure

- `src/api.ts` defines the Nexus Service and its typed input and output.
- `src/activities.ts` implements the Activity that backs the Nexus Operation.
- `src/handler.ts` implements the Operation with `TemporalOperationHandler` and starts the standalone Activity through its typed Activity client.
- `src/worker.ts` runs a Worker that handles both Nexus and Activity tasks.
- `src/starter.ts` starts the Nexus Operation directly from a Temporal Client.

## Run locally

This sample requires the [Temporal CLI build with standalone Nexus Operations](https://github.com/temporalio/cli/releases/tag/v1.7.4-standalone-nexus-operations), with Activity callbacks enabled as shown below.

1. Install dependencies from this directory:

```sh
pnpm install
```

2. Start the Temporal dev server with the two namespaces and required feature flags:

```sh
temporal server start-dev \
--namespace default \
--namespace greeting-handler \
--dynamic-config-value activity.enableCallbacks=true
```

3. Create a Nexus endpoint that routes to the handler Worker:

```sh
temporal operator nexus endpoint create \
--name greeting-endpoint \
--target-namespace greeting-handler \
--target-task-queue greeting-handler-task-queue
```

4. In a second shell, start the Worker:

```sh
TEMPORAL_NAMESPACE=greeting-handler pnpm run worker
```

5. In a third shell, run the starter from the caller namespace:

```sh
TEMPORAL_NAMESPACE=default pnpm run starter
```

Expected output:

```text
Hello, Temporal!
```
51 changes: 51 additions & 0 deletions nexus-standalone-activity/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "nexus-standalone-activity",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "tsc --build",
"build.watch": "tsc --build --watch",
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "eslint .",
"worker": "ts-node src/worker.ts",
"worker.watch": "nodemon --watch src src/worker.ts",
"starter": "ts-node src/starter.ts",
"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.23.0",
"@temporalio/client": "^1.23.0",
"@temporalio/envconfig": "^1.23.0",
"@temporalio/nexus": "^1.23.0",
"@temporalio/worker": "^1.23.0",
"nanoid": "~3.3.12",
"nexus-rpc": "^0.0.3"
},
"devDependencies": {
"@temporalio/testing": "^1.23.0",
"@tsconfig/node22": "^22.0.0",
"@types/mocha": "10.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": "10.x",
"nodemon": "^3.1.7",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
}
}
7 changes: 7 additions & 0 deletions nexus-standalone-activity/src/activities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { GreetInput, GreetOutput } from './api';

export async function greet(input: GreetInput): Promise<GreetOutput> {
return {
message: `Hello, ${input.name}!`,
};
}
13 changes: 13 additions & 0 deletions nexus-standalone-activity/src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as nexus from 'nexus-rpc';

export const greetingService = nexus.service('greetingService', {
greet: nexus.operation<GreetInput, GreetOutput>(),
});

export interface GreetInput {
name: string;
}

export interface GreetOutput {
message: string;
}
22 changes: 22 additions & 0 deletions nexus-standalone-activity/src/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as nexus from 'nexus-rpc';
import * as temporalNexus from '@temporalio/nexus';
import * as activities from './activities';
import { GreetInput, greetingService } from './api';

function activityIdForGreeting(input: GreetInput): string {
return `greeting-${input.name}`;
}

export const greetingServiceHandler = nexus.serviceHandler(greetingService, {
greet: new temporalNexus.TemporalOperationHandler({
async start(_ctx, client, input) {
return await client.typedActivity<typeof activities>().startActivity('greet', {
// Use a business identifier from the Operation input so callers can identify the
// same Activity independently of any individual Nexus request.
id: activityIdForGreeting(input),
args: [input],
startToCloseTimeout: '10s',
});
},
}),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import assert from 'assert';
import { randomUUID } from 'crypto';
import { after, before, describe, it } from 'mocha';
import { TestWorkflowEnvironment } from '@temporalio/testing';
import { Worker } from '@temporalio/worker';
import * as activities from '../activities';
import { greetingService } from '../api';
import { greetingServiceHandler } from '../handler';
import { TASK_QUEUE } from '../shared';

describe('Nexus operation backed by a standalone Activity', () => {
let endpointName: string;
let testEnv: TestWorkflowEnvironment;

before(async () => {
endpointName = `test-nexus-activity-${randomUUID()}`;
const executable = process.env.TEMPORAL_CLI_PATH
? { type: 'existing-path' as const, path: process.env.TEMPORAL_CLI_PATH }
: { type: 'cached-download' as const, version: 'v1.7.4-standalone-nexus-operations' };
testEnv = await TestWorkflowEnvironment.createLocal({
server: {
executable,
extraArgs: ['--dynamic-config-value', 'activity.enableCallbacks=true'],
},
});
});

after(async () => {
await testEnv?.teardown();
});

it('runs the Activity and returns its result through Nexus', async () => {
await testEnv.createNexusEndpoint(endpointName, TASK_QUEUE);
const { client, nativeConnection } = testEnv;

const worker = await Worker.create({
connection: nativeConnection,
namespace: 'default',
taskQueue: TASK_QUEUE,
activities,
nexusServices: [greetingServiceHandler],
});

await worker.runUntil(async () => {
const nexusClient = client.nexus.createServiceClient({
endpoint: endpointName,
service: greetingService,
});

const result = await nexusClient.executeOperation(
greetingService.operations.greet,
{ name: 'Test' },
{ id: randomUUID(), scheduleToCloseTimeout: '10s' },
);

assert.equal(result.message, 'Hello, Test!');
});
});
});
3 changes: 3 additions & 0 deletions nexus-standalone-activity/src/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const ENDPOINT_NAME = 'greeting-endpoint';
export const HANDLER_NAMESPACE = 'greeting-handler';
export const TASK_QUEUE = 'greeting-handler-task-queue';
32 changes: 32 additions & 0 deletions nexus-standalone-activity/src/starter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Client, Connection } from '@temporalio/client';
import { loadClientConnectConfig } from '@temporalio/envconfig';
import { nanoid } from 'nanoid';
import { greetingService } from './api';
import { ENDPOINT_NAME } from './shared';

async function run() {
const config = loadClientConnectConfig();
const connection = await Connection.connect(config.connectionOptions);
const client = new Client({ connection, namespace: config.namespace ?? 'default' });

const nexusClient = client.nexus.createServiceClient({
endpoint: ENDPOINT_NAME,
service: greetingService,
});

const result = await nexusClient.executeOperation(
greetingService.operations.greet,
{ name: 'Temporal' },
{
id: nanoid(),
scheduleToCloseTimeout: '10s',
},
);

console.log(result.message);
}

run().catch((err) => {
console.error(err);
process.exit(1);
});
Loading