From c3ce8fae3ea83e8be0d83f042efe3d156f6255f8 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Mon, 16 Sep 2024 16:40:02 -0400 Subject: [PATCH] Sample for message passing docs (#382) * Introduction to message passing sample * Rename updates-and-signals => message-passing --- .scripts/list-of-samples.json | 2 +- .../execute-update/.eslintignore | 0 .../execute-update/.eslintrc.js | 0 .../execute-update/.gitignore | 0 .../execute-update/.npmrc | 0 .../execute-update/.nvmrc | 0 .../execute-update/.post-create | 0 .../execute-update/.prettierignore | 0 .../execute-update/.prettierrc | 0 .../execute-update/README.md | 0 .../execute-update/package.json | 0 .../execute-update/src/client.ts | 0 .../execute-update/src/worker.ts | 0 .../execute-update/src/workflows.ts | 0 .../execute-update/tsconfig.json | 0 .../introduction}/.eslintignore | 0 .../introduction}/.eslintrc.js | 0 .../introduction}/.gitignore | 0 .../introduction}/.npmrc | 0 .../introduction}/.nvmrc | 0 .../introduction}/.post-create | 0 .../introduction}/.prettierignore | 0 .../introduction}/.prettierrc | 0 message-passing/introduction/README.md | 10 ++ message-passing/introduction/package.json | 48 ++++++ message-passing/introduction/src/client.ts | 53 ++++++ .../introduction/src/test/workflows.test.ts | 154 ++++++++++++++++++ message-passing/introduction/src/worker.ts | 23 +++ message-passing/introduction/src/workflows.ts | 135 +++++++++++++++ .../introduction}/tsconfig.json | 0 .../safe-message-handlers/.eslintignore | 3 + .../safe-message-handlers/.eslintrc.js | 48 ++++++ .../safe-message-handlers/.gitignore | 2 + message-passing/safe-message-handlers/.npmrc | 1 + message-passing/safe-message-handlers/.nvmrc | 1 + .../safe-message-handlers/.post-create | 18 ++ .../safe-message-handlers/.prettierignore | 1 + .../safe-message-handlers/.prettierrc | 2 + .../safe-message-handlers/README.md | 0 .../safe-message-handlers/package.json | 0 .../safe-message-handlers/src/activities.ts | 0 .../safe-message-handlers/src/client.ts | 0 .../src/cluster-manager.ts | 0 .../src/run-simulation.ts | 0 .../src/test/workflows.test.ts | 0 .../safe-message-handlers/src/types.ts | 0 .../safe-message-handlers/src/worker.ts | 0 .../safe-message-handlers/src/workflows.ts | 0 message-passing/tsconfig.json | 12 ++ 49 files changed, 512 insertions(+), 1 deletion(-) rename {updates-and-signals => message-passing}/execute-update/.eslintignore (100%) rename {updates-and-signals => message-passing}/execute-update/.eslintrc.js (100%) rename {updates-and-signals => message-passing}/execute-update/.gitignore (100%) rename {updates-and-signals => message-passing}/execute-update/.npmrc (100%) rename {updates-and-signals => message-passing}/execute-update/.nvmrc (100%) rename {updates-and-signals => message-passing}/execute-update/.post-create (100%) rename {updates-and-signals => message-passing}/execute-update/.prettierignore (100%) rename {updates-and-signals => message-passing}/execute-update/.prettierrc (100%) rename {updates-and-signals => message-passing}/execute-update/README.md (100%) rename {updates-and-signals => message-passing}/execute-update/package.json (100%) rename {updates-and-signals => message-passing}/execute-update/src/client.ts (100%) rename {updates-and-signals => message-passing}/execute-update/src/worker.ts (100%) rename {updates-and-signals => message-passing}/execute-update/src/workflows.ts (100%) rename {updates-and-signals => message-passing}/execute-update/tsconfig.json (100%) rename {updates-and-signals/safe-message-handlers => message-passing/introduction}/.eslintignore (100%) rename {updates-and-signals/safe-message-handlers => message-passing/introduction}/.eslintrc.js (100%) rename {updates-and-signals/safe-message-handlers => message-passing/introduction}/.gitignore (100%) rename {updates-and-signals/safe-message-handlers => message-passing/introduction}/.npmrc (100%) rename {updates-and-signals/safe-message-handlers => message-passing/introduction}/.nvmrc (100%) rename {updates-and-signals/safe-message-handlers => message-passing/introduction}/.post-create (100%) rename {updates-and-signals/safe-message-handlers => message-passing/introduction}/.prettierignore (100%) rename {updates-and-signals/safe-message-handlers => message-passing/introduction}/.prettierrc (100%) create mode 100644 message-passing/introduction/README.md create mode 100644 message-passing/introduction/package.json create mode 100644 message-passing/introduction/src/client.ts create mode 100644 message-passing/introduction/src/test/workflows.test.ts create mode 100644 message-passing/introduction/src/worker.ts create mode 100644 message-passing/introduction/src/workflows.ts rename {updates-and-signals => message-passing/introduction}/tsconfig.json (100%) create mode 100644 message-passing/safe-message-handlers/.eslintignore create mode 100644 message-passing/safe-message-handlers/.eslintrc.js create mode 100644 message-passing/safe-message-handlers/.gitignore create mode 100644 message-passing/safe-message-handlers/.npmrc create mode 100644 message-passing/safe-message-handlers/.nvmrc create mode 100644 message-passing/safe-message-handlers/.post-create create mode 100644 message-passing/safe-message-handlers/.prettierignore create mode 100644 message-passing/safe-message-handlers/.prettierrc rename {updates-and-signals => message-passing}/safe-message-handlers/README.md (100%) rename {updates-and-signals => message-passing}/safe-message-handlers/package.json (100%) rename {updates-and-signals => message-passing}/safe-message-handlers/src/activities.ts (100%) rename {updates-and-signals => message-passing}/safe-message-handlers/src/client.ts (100%) rename {updates-and-signals => message-passing}/safe-message-handlers/src/cluster-manager.ts (100%) rename {updates-and-signals => message-passing}/safe-message-handlers/src/run-simulation.ts (100%) rename {updates-and-signals => message-passing}/safe-message-handlers/src/test/workflows.test.ts (100%) rename {updates-and-signals => message-passing}/safe-message-handlers/src/types.ts (100%) rename {updates-and-signals => message-passing}/safe-message-handlers/src/worker.ts (100%) rename {updates-and-signals => message-passing}/safe-message-handlers/src/workflows.ts (100%) create mode 100644 message-passing/tsconfig.json diff --git a/.scripts/list-of-samples.json b/.scripts/list-of-samples.json index 86a9f58b..88992e6b 100644 --- a/.scripts/list-of-samples.json +++ b/.scripts/list-of-samples.json @@ -19,6 +19,7 @@ "hello-world-js", "hello-world-mtls", "interceptors-opentelemetry", + "message-passing", "monorepo-folders", "mutex", "nestjs-exchange-rates", @@ -37,7 +38,6 @@ "state", "timer-examples", "timer-progress", - "updates-and-signals", "vscode-debugger", "worker-specific-task-queues", "worker-versioning" diff --git a/updates-and-signals/execute-update/.eslintignore b/message-passing/execute-update/.eslintignore similarity index 100% rename from updates-and-signals/execute-update/.eslintignore rename to message-passing/execute-update/.eslintignore diff --git a/updates-and-signals/execute-update/.eslintrc.js b/message-passing/execute-update/.eslintrc.js similarity index 100% rename from updates-and-signals/execute-update/.eslintrc.js rename to message-passing/execute-update/.eslintrc.js diff --git a/updates-and-signals/execute-update/.gitignore b/message-passing/execute-update/.gitignore similarity index 100% rename from updates-and-signals/execute-update/.gitignore rename to message-passing/execute-update/.gitignore diff --git a/updates-and-signals/execute-update/.npmrc b/message-passing/execute-update/.npmrc similarity index 100% rename from updates-and-signals/execute-update/.npmrc rename to message-passing/execute-update/.npmrc diff --git a/updates-and-signals/execute-update/.nvmrc b/message-passing/execute-update/.nvmrc similarity index 100% rename from updates-and-signals/execute-update/.nvmrc rename to message-passing/execute-update/.nvmrc diff --git a/updates-and-signals/execute-update/.post-create b/message-passing/execute-update/.post-create similarity index 100% rename from updates-and-signals/execute-update/.post-create rename to message-passing/execute-update/.post-create diff --git a/updates-and-signals/execute-update/.prettierignore b/message-passing/execute-update/.prettierignore similarity index 100% rename from updates-and-signals/execute-update/.prettierignore rename to message-passing/execute-update/.prettierignore diff --git a/updates-and-signals/execute-update/.prettierrc b/message-passing/execute-update/.prettierrc similarity index 100% rename from updates-and-signals/execute-update/.prettierrc rename to message-passing/execute-update/.prettierrc diff --git a/updates-and-signals/execute-update/README.md b/message-passing/execute-update/README.md similarity index 100% rename from updates-and-signals/execute-update/README.md rename to message-passing/execute-update/README.md diff --git a/updates-and-signals/execute-update/package.json b/message-passing/execute-update/package.json similarity index 100% rename from updates-and-signals/execute-update/package.json rename to message-passing/execute-update/package.json diff --git a/updates-and-signals/execute-update/src/client.ts b/message-passing/execute-update/src/client.ts similarity index 100% rename from updates-and-signals/execute-update/src/client.ts rename to message-passing/execute-update/src/client.ts diff --git a/updates-and-signals/execute-update/src/worker.ts b/message-passing/execute-update/src/worker.ts similarity index 100% rename from updates-and-signals/execute-update/src/worker.ts rename to message-passing/execute-update/src/worker.ts diff --git a/updates-and-signals/execute-update/src/workflows.ts b/message-passing/execute-update/src/workflows.ts similarity index 100% rename from updates-and-signals/execute-update/src/workflows.ts rename to message-passing/execute-update/src/workflows.ts diff --git a/updates-and-signals/execute-update/tsconfig.json b/message-passing/execute-update/tsconfig.json similarity index 100% rename from updates-and-signals/execute-update/tsconfig.json rename to message-passing/execute-update/tsconfig.json diff --git a/updates-and-signals/safe-message-handlers/.eslintignore b/message-passing/introduction/.eslintignore similarity index 100% rename from updates-and-signals/safe-message-handlers/.eslintignore rename to message-passing/introduction/.eslintignore diff --git a/updates-and-signals/safe-message-handlers/.eslintrc.js b/message-passing/introduction/.eslintrc.js similarity index 100% rename from updates-and-signals/safe-message-handlers/.eslintrc.js rename to message-passing/introduction/.eslintrc.js diff --git a/updates-and-signals/safe-message-handlers/.gitignore b/message-passing/introduction/.gitignore similarity index 100% rename from updates-and-signals/safe-message-handlers/.gitignore rename to message-passing/introduction/.gitignore diff --git a/updates-and-signals/safe-message-handlers/.npmrc b/message-passing/introduction/.npmrc similarity index 100% rename from updates-and-signals/safe-message-handlers/.npmrc rename to message-passing/introduction/.npmrc diff --git a/updates-and-signals/safe-message-handlers/.nvmrc b/message-passing/introduction/.nvmrc similarity index 100% rename from updates-and-signals/safe-message-handlers/.nvmrc rename to message-passing/introduction/.nvmrc diff --git a/updates-and-signals/safe-message-handlers/.post-create b/message-passing/introduction/.post-create similarity index 100% rename from updates-and-signals/safe-message-handlers/.post-create rename to message-passing/introduction/.post-create diff --git a/updates-and-signals/safe-message-handlers/.prettierignore b/message-passing/introduction/.prettierignore similarity index 100% rename from updates-and-signals/safe-message-handlers/.prettierignore rename to message-passing/introduction/.prettierignore diff --git a/updates-and-signals/safe-message-handlers/.prettierrc b/message-passing/introduction/.prettierrc similarity index 100% rename from updates-and-signals/safe-message-handlers/.prettierrc rename to message-passing/introduction/.prettierrc diff --git a/message-passing/introduction/README.md b/message-passing/introduction/README.md new file mode 100644 index 00000000..cbeb9308 --- /dev/null +++ b/message-passing/introduction/README.md @@ -0,0 +1,10 @@ +### Introduction + +An introduction to Queries, Signals, and Updates. + +### Running this sample + +1. `temporal server start-dev --dynamic-config-value frontend.enableUpdateWorkflowExecution=true` to start [Temporal Server](https://github.com/temporalio/cli/#installation) with Workflow Updates enabled. +1. `npm install` to install dependencies. +1. `npm run start.watch` to start the Worker. +1. In another shell, `npm run workflow` to run the Workflow Client. diff --git a/message-passing/introduction/package.json b/message-passing/introduction/package.json new file mode 100644 index 00000000..ea55bf56 --- /dev/null +++ b/message-passing/introduction/package.json @@ -0,0 +1,48 @@ +{ + "name": "temporal-update", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc --build", + "build.watch": "tsc --build --watch", + "lint": "eslint .", + "start": "ts-node src/worker.ts", + "start.watch": "nodemon src/worker.ts", + "workflow": "ts-node src/client.ts", + "format": "prettier --config .prettierrc 'src/**/*.ts' --write", + "test": "mocha --exit --require ts-node/register src/test/*.test.ts" + }, + "nodemonConfig": { + "execMap": { + "ts": "ts-node" + }, + "ext": "ts", + "watch": [ + "src" + ] + }, + "dependencies": { + "@temporalio/activity": "^1.11.1", + "@temporalio/client": "^1.11.1", + "@temporalio/worker": "^1.11.1", + "@temporalio/workflow": "^1.11.1", + "async-mutex": "^0.5.0", + "nanoid": "3.x" + }, + "devDependencies": { + "@temporalio/testing": "^1.11.1", + "@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" + } +} diff --git a/message-passing/introduction/src/client.ts b/message-passing/introduction/src/client.ts new file mode 100644 index 00000000..3f1a3d0b --- /dev/null +++ b/message-passing/introduction/src/client.ts @@ -0,0 +1,53 @@ +// @@@SNIPSTART typescript-message-passing-introduction +import * as cl from '@temporalio/client'; +import * as greetingWorkflow from './workflows'; +import { Language } from './workflows'; +import { nanoid } from 'nanoid'; + +export async function run() { + const connection = await cl.Connection.connect({ address: 'localhost:7233' }); + const client = new cl.Client({ connection }); + const handle = await client.workflow.start(greetingWorkflow.greetingWorkflow, { + taskQueue: 'my-task-queue', + args: [], + workflowId: `messages-introduction-${nanoid()}`, + }); + + const supportedLanguages = await handle.query(greetingWorkflow.getLanguages, { includeUnsupported: false }); + console.log(`supported languages: ${supportedLanguages}`); + + // Use executeUpdate to change the language + let previousLanguage = await handle.executeUpdate(greetingWorkflow.setLanguage, { + args: [Language.CHINESE], + }); + + // Send a query + let currentLanguage = await handle.query(greetingWorkflow.getLanguage); + console.log(`language changed: ${previousLanguage} -> ${currentLanguage}`); + + // Use startUpdate followed by handle.result() to change the language + const updateHandle = await handle.startUpdate(greetingWorkflow.setLanguage, { + args: [Language.ENGLISH], + waitForStage: cl.WorkflowUpdateStage.ACCEPTED, + }); + previousLanguage = await updateHandle.result(); + currentLanguage = await handle.query(greetingWorkflow.getLanguage); + console.log(`language changed: ${previousLanguage} -> ${currentLanguage}`); + + // Use an async update handler that calls an activity to change the language + previousLanguage = await handle.executeUpdate(greetingWorkflow.setLanguageUsingActivity, { + args: [Language.ARABIC], + }); + currentLanguage = await handle.query(greetingWorkflow.getLanguage); + console.log(`language changed: ${previousLanguage} -> ${currentLanguage}`); + + // Send a signal + await handle.signal(greetingWorkflow.approve, { name: '' }); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); +// @@@SNIPEND diff --git a/message-passing/introduction/src/test/workflows.test.ts b/message-passing/introduction/src/test/workflows.test.ts new file mode 100644 index 00000000..8943189d --- /dev/null +++ b/message-passing/introduction/src/test/workflows.test.ts @@ -0,0 +1,154 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { before, describe, it } from 'mocha'; +import * as wo from '@temporalio/worker'; +import * as greetingWorkflow from '../workflows'; +import { Language } from '../workflows'; +import assert from 'assert'; +import { nanoid } from 'nanoid'; +import { WorkflowUpdateFailedError } from '@temporalio/client'; + +const taskQueue = 'message-passing-introduction'; + +describe('greeting workflow', function () { + this.timeout(10000); + let worker: wo.Worker; + let env: TestWorkflowEnvironment; + let workflowBundle: wo.WorkflowBundleWithSourceMap; + + before(async function () { + wo.Runtime.install({ logger: new wo.DefaultLogger('WARN') }); + env = await TestWorkflowEnvironment.createLocal(); + + workflowBundle = await wo.bundleWorkflowCode({ + workflowsPath: require.resolve('../workflows'), + logger: new wo.DefaultLogger('WARN'), + }); + }); + + beforeEach(async function () { + worker = await wo.Worker.create({ + connection: env.nativeConnection, + workflowBundle, + taskQueue, + activities: greetingWorkflow.activities, + }); + }); + + after(async function () { + await env.teardown(); + }); + + it('can be queried for supported languages', async function () { + const wfResult = await worker.runUntil(async () => { + const wfHandle = await env.client.workflow.start(greetingWorkflow.greetingWorkflow, { + taskQueue, + workflowId: nanoid(), + }); + const supportedLanguages = await wfHandle.query(greetingWorkflow.getLanguages, { + includeUnsupported: false, + }); + assert.deepEqual(supportedLanguages, [Language.CHINESE, Language.ENGLISH]); + await wfHandle.signal(greetingWorkflow.approve, { name: 'test-approver' }); + return await wfHandle.result(); + }); + assert.equal(wfResult, 'Hello, world'); + }); + + it('can be queried for unsupported language', async function () { + const wfResult = await worker.runUntil(async () => { + const wfHandle = await env.client.workflow.start(greetingWorkflow.greetingWorkflow, { + taskQueue, + workflowId: nanoid(), + }); + const allLanguages = await wfHandle.query(greetingWorkflow.getLanguages, { + includeUnsupported: true, + }); + assert.deepEqual(allLanguages, [ + Language.ARABIC, + Language.CHINESE, + Language.ENGLISH, + Language.FRENCH, + Language.HINDI, + Language.PORTUGUESE, + Language.SPANISH, + ]); + await wfHandle.signal(greetingWorkflow.approve, { name: 'test-approver' }); + return await wfHandle.result(); + }); + assert.equal(wfResult, 'Hello, world'); + }); + + it('can be updated to change the language', async function () { + const wfResult = await worker.runUntil(async () => { + const wfHandle = await env.client.workflow.start(greetingWorkflow.greetingWorkflow, { + taskQueue, + workflowId: nanoid(), + }); + + const currentLanguage = await wfHandle.query(greetingWorkflow.getLanguage); + assert.equal(currentLanguage, Language.ENGLISH); + + await wfHandle.executeUpdate(greetingWorkflow.setLanguage, { + args: [Language.CHINESE], + }); + + const updatedLanguage = await wfHandle.query(greetingWorkflow.getLanguage); + assert.equal(updatedLanguage, Language.CHINESE); + + await wfHandle.signal(greetingWorkflow.approve, { name: 'test-approver' }); + return await wfHandle.result(); + }); + assert.equal(wfResult, '你好,世界'); + }); + + it('rejects an invalid language', async function () { + const wfResult = await worker.runUntil(async () => { + const wfHandle = await env.client.workflow.start(greetingWorkflow.greetingWorkflow, { + taskQueue, + workflowId: nanoid(), + }); + + const currentLanguage = await wfHandle.query(greetingWorkflow.getLanguage); + assert.equal(currentLanguage, Language.ENGLISH); + + try { + await wfHandle.executeUpdate(greetingWorkflow.setLanguage, { + args: [Language.PORTUGUESE], + }); + assert.fail('Expected an error to be thrown'); + } catch (err) { + assert(err instanceof WorkflowUpdateFailedError, 'Expected WorkflowUpdateFailedError'); + } + + const updatedLanguage = await wfHandle.query(greetingWorkflow.getLanguage); + assert.equal(updatedLanguage, Language.ENGLISH); + + await wfHandle.signal(greetingWorkflow.approve, { name: 'test-approver' }); + return await wfHandle.result(); + }); + assert.equal(wfResult, 'Hello, world'); + }); + + it('can be updated to change the language using an activity', async function () { + const wfResult = await worker.runUntil(async () => { + const wfHandle = await env.client.workflow.start(greetingWorkflow.greetingWorkflow, { + taskQueue, + workflowId: nanoid(), + }); + + const currentLanguage = await wfHandle.query(greetingWorkflow.getLanguage); + assert.equal(currentLanguage, Language.ENGLISH); + + await wfHandle.executeUpdate(greetingWorkflow.setLanguageUsingActivity, { + args: [Language.PORTUGUESE], + }); + + const updatedLanguage = await wfHandle.query(greetingWorkflow.getLanguage); + assert.equal(updatedLanguage, Language.PORTUGUESE); + + await wfHandle.signal(greetingWorkflow.approve, { name: 'test-approver' }); + return await wfHandle.result(); + }); + assert.equal(wfResult, 'Olá mundo'); + }); +}); diff --git a/message-passing/introduction/src/worker.ts b/message-passing/introduction/src/worker.ts new file mode 100644 index 00000000..f3bb628c --- /dev/null +++ b/message-passing/introduction/src/worker.ts @@ -0,0 +1,23 @@ +// @@@SNIPSTART typescript-message-passing-introduction +import * as wo from '@temporalio/worker'; +import { activities } from './workflows'; + +async function run() { + const connection = await wo.NativeConnection.connect({ + address: 'localhost:7233', + }); + const worker = await wo.Worker.create({ + connection, + namespace: 'default', + taskQueue: 'my-task-queue', + workflowsPath: require.resolve('./workflows'), + activities, + }); + await worker.run(); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); +// @@@SNIPEND diff --git a/message-passing/introduction/src/workflows.ts b/message-passing/introduction/src/workflows.ts new file mode 100644 index 00000000..72d14339 --- /dev/null +++ b/message-passing/introduction/src/workflows.ts @@ -0,0 +1,135 @@ +import * as wf from '@temporalio/workflow'; +import { Mutex } from 'async-mutex'; + +export enum Language { + ARABIC = 'ARABIC', + CHINESE = 'CHINESE', + ENGLISH = 'ENGLISH', + FRENCH = 'FRENCH', + HINDI = 'HINDI', + PORTUGUESE = 'PORTUGUESE', + SPANISH = 'SPANISH', +} + +interface GetLanguagesInput { + includeUnsupported: boolean; +} + +interface ApproveInput { + name: string; +} + +// 👉 Use the objects returned by defineQuery/defineSignal/defineUpdate to set +// the message handlers in Workflow code, and to send messages from Client code. +export const getLanguages = wf.defineQuery('getLanguages'); +export const approve = wf.defineSignal<[ApproveInput]>('approve'); +export const setLanguage = wf.defineUpdate('setLanguage'); +export const setLanguageUsingActivity = wf.defineUpdate('setLanguageUsingActivity'); +export const getLanguage = wf.defineQuery('getLanguage'); + +export async function greetingWorkflow(): Promise { + // The workflow has built-in support for two languages only (see remote + // greeting service below for support for more languages). + const greetings: Partial> = { + [Language.CHINESE]: '你好,世界', + [Language.ENGLISH]: 'Hello, world', + }; + + let approvedForRelease = false; + let approverName: string | undefined; + let language = Language.ENGLISH; + + wf.setHandler(getLanguages, (input: GetLanguagesInput): Language[] => { + // 👉 A Query handler returns a value: it must not mutate the Workflow state + // and cannot perform async operations. + if (input.includeUnsupported) { + return Object.values(Language); + } else { + return Object.keys(greetings) as Language[]; + } + }); + + wf.setHandler(approve, (input) => { + // 👉 A Signal handler mutates the Workflow state but cannot return a value. + approvedForRelease = true; + approverName = input.name; + }); + + wf.setHandler( + setLanguage, + (newLanguage: Language) => { + // 👉 An Update handler can mutate the Workflow state and return a value. + const previousLanguage = language; + language = newLanguage; + return previousLanguage; + }, + { + validator: (newLanguage: Language) => { + // 👉 Update validators are optional + if (!(newLanguage in greetings)) { + throw new wf.ApplicationFailure(`${newLanguage} is not supported`); + } + }, + } + ); + + wf.setHandler(getLanguage, () => { + return language; + }); + + /** + * Async update handler featuring an Activity that calls the remote greeting service + */ + + const lock = new Mutex(); + wf.setHandler(setLanguageUsingActivity, async (newLanguage) => { + // 👉 An Update handler can mutate the Workflow state and return a value. + // 👉 Since this update handler is async, it can execute an activity. + if (!(newLanguage in greetings)) { + await lock.runExclusive(async () => { + // 👉 We use a lock so that, if this handler is executed multiple times, + // each execution can schedule the activity only when the previously + // scheduled activity has completed. This ensures that multiple calls to + // setLanguageUsingActivity are processed in order. + if (!(newLanguage in greetings)) { + const greeting = await callGreetingService(newLanguage); + if (!greeting) { + // 👉 An update validator cannot be async, so cannot be used to check that the remote + // call_greeting_service supports the requested language. Raising ApplicationError + // will fail the Update, but the WorkflowExecutionUpdateAccepted event will still be + // added to history. + throw new wf.ApplicationFailure(`${newLanguage} is not supported by the greeting service`); + } + greetings[newLanguage] = greeting; + } + }); + } + const previousLanguage = language; + language = newLanguage; + return previousLanguage; + }); + + await wf.condition(() => approvedForRelease); + return greetings[language]!; +} + +export const activities = { + callGreetingService: async (toLanguage: Language): Promise => { + // The remote greeting service supports more languages. + const greetings = { + [Language.ENGLISH]: 'Hello, world', + [Language.CHINESE]: '你好,世界', + [Language.FRENCH]: 'Bonjour, monde', + [Language.SPANISH]: 'Hola mundo', + [Language.PORTUGUESE]: 'Olá mundo', + [Language.ARABIC]: 'مرحبا بالعالم', + [Language.HINDI]: 'नमस्ते दुनिया', + }; + await new Promise((resolve) => setTimeout(resolve, 200)); // Simulate a network call + return greetings[toLanguage]; + }, +}; + +const { callGreetingService } = wf.proxyActivities({ + startToCloseTimeout: '1 minute', +}); diff --git a/updates-and-signals/tsconfig.json b/message-passing/introduction/tsconfig.json similarity index 100% rename from updates-and-signals/tsconfig.json rename to message-passing/introduction/tsconfig.json diff --git a/message-passing/safe-message-handlers/.eslintignore b/message-passing/safe-message-handlers/.eslintignore new file mode 100644 index 00000000..7bd99a41 --- /dev/null +++ b/message-passing/safe-message-handlers/.eslintignore @@ -0,0 +1,3 @@ +node_modules +lib +.eslintrc.js \ No newline at end of file diff --git a/message-passing/safe-message-handlers/.eslintrc.js b/message-passing/safe-message-handlers/.eslintrc.js new file mode 100644 index 00000000..b8251a06 --- /dev/null +++ b/message-passing/safe-message-handlers/.eslintrc.js @@ -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}`]), + ], + }, + }, + ], +}; diff --git a/message-passing/safe-message-handlers/.gitignore b/message-passing/safe-message-handlers/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/message-passing/safe-message-handlers/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/message-passing/safe-message-handlers/.npmrc b/message-passing/safe-message-handlers/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/message-passing/safe-message-handlers/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/message-passing/safe-message-handlers/.nvmrc b/message-passing/safe-message-handlers/.nvmrc new file mode 100644 index 00000000..b6a7d89c --- /dev/null +++ b/message-passing/safe-message-handlers/.nvmrc @@ -0,0 +1 @@ +16 diff --git a/message-passing/safe-message-handlers/.post-create b/message-passing/safe-message-handlers/.post-create new file mode 100644 index 00000000..a682bb78 --- /dev/null +++ b/message-passing/safe-message-handlers/.post-create @@ -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 16+: + + Mac: {cyan brew install node@16} + 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} diff --git a/message-passing/safe-message-handlers/.prettierignore b/message-passing/safe-message-handlers/.prettierignore new file mode 100644 index 00000000..7951405f --- /dev/null +++ b/message-passing/safe-message-handlers/.prettierignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/message-passing/safe-message-handlers/.prettierrc b/message-passing/safe-message-handlers/.prettierrc new file mode 100644 index 00000000..965d50bf --- /dev/null +++ b/message-passing/safe-message-handlers/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 120 +singleQuote: true diff --git a/updates-and-signals/safe-message-handlers/README.md b/message-passing/safe-message-handlers/README.md similarity index 100% rename from updates-and-signals/safe-message-handlers/README.md rename to message-passing/safe-message-handlers/README.md diff --git a/updates-and-signals/safe-message-handlers/package.json b/message-passing/safe-message-handlers/package.json similarity index 100% rename from updates-and-signals/safe-message-handlers/package.json rename to message-passing/safe-message-handlers/package.json diff --git a/updates-and-signals/safe-message-handlers/src/activities.ts b/message-passing/safe-message-handlers/src/activities.ts similarity index 100% rename from updates-and-signals/safe-message-handlers/src/activities.ts rename to message-passing/safe-message-handlers/src/activities.ts diff --git a/updates-and-signals/safe-message-handlers/src/client.ts b/message-passing/safe-message-handlers/src/client.ts similarity index 100% rename from updates-and-signals/safe-message-handlers/src/client.ts rename to message-passing/safe-message-handlers/src/client.ts diff --git a/updates-and-signals/safe-message-handlers/src/cluster-manager.ts b/message-passing/safe-message-handlers/src/cluster-manager.ts similarity index 100% rename from updates-and-signals/safe-message-handlers/src/cluster-manager.ts rename to message-passing/safe-message-handlers/src/cluster-manager.ts diff --git a/updates-and-signals/safe-message-handlers/src/run-simulation.ts b/message-passing/safe-message-handlers/src/run-simulation.ts similarity index 100% rename from updates-and-signals/safe-message-handlers/src/run-simulation.ts rename to message-passing/safe-message-handlers/src/run-simulation.ts diff --git a/updates-and-signals/safe-message-handlers/src/test/workflows.test.ts b/message-passing/safe-message-handlers/src/test/workflows.test.ts similarity index 100% rename from updates-and-signals/safe-message-handlers/src/test/workflows.test.ts rename to message-passing/safe-message-handlers/src/test/workflows.test.ts diff --git a/updates-and-signals/safe-message-handlers/src/types.ts b/message-passing/safe-message-handlers/src/types.ts similarity index 100% rename from updates-and-signals/safe-message-handlers/src/types.ts rename to message-passing/safe-message-handlers/src/types.ts diff --git a/updates-and-signals/safe-message-handlers/src/worker.ts b/message-passing/safe-message-handlers/src/worker.ts similarity index 100% rename from updates-and-signals/safe-message-handlers/src/worker.ts rename to message-passing/safe-message-handlers/src/worker.ts diff --git a/updates-and-signals/safe-message-handlers/src/workflows.ts b/message-passing/safe-message-handlers/src/workflows.ts similarity index 100% rename from updates-and-signals/safe-message-handlers/src/workflows.ts rename to message-passing/safe-message-handlers/src/workflows.ts diff --git a/message-passing/tsconfig.json b/message-passing/tsconfig.json new file mode 100644 index 00000000..6ff187f6 --- /dev/null +++ b/message-passing/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node16/tsconfig.json", + "version": "4.4.2", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["src/**/*.ts"] +}