Skip to content
Draft
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
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
2 changes: 2 additions & 0 deletions nexus-standalone-activity/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
lib
.eslintrc.js
43 changes: 43 additions & 0 deletions nexus-standalone-activity/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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: {
'@typescript-eslint/no-floating-promises': 'error',
'deprecation/deprecation': 'warn',
'object-shorthand': ['error', 'always'],
'@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}`]),
],
},
},
],
};
3 changes: 3 additions & 0 deletions nexus-standalone-activity/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
lib
node_modules
tsconfig.tsbuildinfo
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
13 changes: 13 additions & 0 deletions nexus-standalone-activity/.post-create
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Use Node version 20.3+ (v22.x is recommended):

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

Install this sample's main-branch SDK dependencies:

{cyan pnpm install}

Then follow README.md to start a compatible Temporal dev server, create the Nexus endpoint, and run:

{cyan pnpm run worker}
{cyan pnpm run starter}
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": "link:../../../sdk-typescript/packages/activity",
"@temporalio/client": "link:../../../sdk-typescript/packages/client",
"@temporalio/envconfig": "link:../../../sdk-typescript/packages/envconfig",
"@temporalio/nexus": "link:../../../sdk-typescript/packages/nexus",
"@temporalio/worker": "link:../../../sdk-typescript/packages/worker",
"nanoid": "~3.3.12",
"nexus-rpc": "^0.0.2"
},
"devDependencies": {
"@temporalio/testing": "link:../../../sdk-typescript/packages/testing",
"@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);
});
28 changes: 28 additions & 0 deletions nexus-standalone-activity/src/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { loadClientConnectConfig } from '@temporalio/envconfig';
import { NativeConnection, Worker } from '@temporalio/worker';
import * as activities from './activities';
import { greetingServiceHandler } from './handler';
import { HANDLER_NAMESPACE, TASK_QUEUE } from './shared';

async function run() {
const config = loadClientConnectConfig();
const connection = await NativeConnection.connect(config.connectionOptions);
try {
const worker = await Worker.create({
connection,
namespace: config.namespace ?? HANDLER_NAMESPACE,
taskQueue: TASK_QUEUE,
activities,
nexusServices: [greetingServiceHandler],
});

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

run().catch((err) => {
console.error(err);
process.exit(1);
});
13 changes: 13 additions & 0 deletions nexus-standalone-activity/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"version": "5.6.3",
"compilerOptions": {
"lib": ["es2021"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"rootDir": "./src",
"outDir": "./lib"
},
"include": ["src/**/*.ts"]
}
Loading
Loading