diff --git a/daprdocs/content/en/js-sdk-docs/js-workflow/_index.md b/daprdocs/content/en/js-sdk-docs/js-workflow/_index.md new file mode 100644 index 00000000..59431545 --- /dev/null +++ b/daprdocs/content/en/js-sdk-docs/js-workflow/_index.md @@ -0,0 +1,162 @@ +--- +type: docs +title: "How to: Author and manage Dapr Workflow in the JavaScript SDK" +linkTitle: "How to: Author and manage workflows" +weight: 20000 +description: How to get up and running with workflows using the Dapr JavaScript SDK +--- + +{{% alert title="Note" color="primary" %}} +Dapr Workflow is currently in beta. [See known limitations for {{% dapr-latest-version cli="true" %}}]({{< ref "workflow-overview.md#limitations" >}}). +{{% /alert %}} + +Let’s create a Dapr workflow and invoke it using the console. With the [provided workflow example](https://github.com/dapr/js-sdk/tree/main/examples/workflow), you will: + +- Execute the workflow instance using the [JavaScript workflow worker](https://github.com/dapr/js-sdk/tree/main/src/workflow/runtime/WorkflowRuntime.ts) +- Utilize the JavaScript workflow client and API calls to [start and terminate workflow instances](https://github.com/dapr/js-sdk/tree/main/src/workflow/client/DaprWorkflowClient.ts) + +This example uses the default configuration from `dapr init` in [self-hosted mode](https://github.com/dapr/cli#install-dapr-on-your-local-machine-self-hosted). + +## Prerequisites + +- [Dapr CLI and initialized environment](https://docs.dapr.io/getting-started). +- [Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), +- [Docker Desktop](https://www.docker.com/products/docker-desktop) + +- Verify you're using the latest proto bindings + +## Set up the environment + +Clone the JavaScript SDK repo and navigate into it. + +```bash +git clone https://github.com/dapr/js-sdk +cd js-sdk +``` + +From the JavaScript SDK root directory, navigate to the Dapr Workflow example. + +```bash +cd examples/workflow/authoring +``` + +Run the following command to install the requirements for running this workflow sample with the Dapr JavaScript SDK. + +```bash +npm install +``` + +## Run the `activity-sequence.ts` + +The `activity-sequence` file registers a workflow and an activity with the Dapr Workflow runtime. The workflow is a sequence of activities that are executed in order. We use DaprWorkflowClient to schedule a new workflow instance and wait for it to complete. + +```typescript +const daprHost = "localhost"; +const daprPort = "50001"; +const workflowClient = new DaprWorkflowClient({ + daprHost, + daprPort, +}); +const workflowRuntime = new WorkflowRuntime({ + daprHost, + daprPort, +}); + +const hello = async (_: WorkflowActivityContext, name: string) => { + return `Hello ${name}!`; +}; + +const sequence: TWorkflow = async function* (ctx: WorkflowContext): any { + const cities: string[] = []; + + const result1 = yield ctx.callActivity(hello, "Tokyo"); + cities.push(result1); + const result2 = yield ctx.callActivity(hello, "Seattle"); + cities.push(result2); + const result3 = yield ctx.callActivity(hello, "London"); + cities.push(result3); + + return cities; +}; + +workflowRuntime.registerWorkflow(sequence).registerActivity(hello); + +// Wrap the worker startup in a try-catch block to handle any errors during startup +try { + await workflowRuntime.start(); + console.log("Workflow runtime started successfully"); +} catch (error) { + console.error("Error starting workflow runtime:", error); +} + +// Schedule a new orchestration +try { + const id = await workflowClient.scheduleNewWorkflow(sequence); + console.log(`Orchestration scheduled with ID: ${id}`); + + // Wait for orchestration completion + const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30); + + console.log(`Orchestration completed! Result: ${state?.serializedOutput}`); +} catch (error) { + console.error("Error scheduling or waiting for orchestration:", error); +} +``` + +In the code above: + +- `workflowRuntime.registerWorkflow(sequence)` registers `sequence` as a workflow in the Dapr Workflow runtime. +- `await workflowRuntime.start();` builds and starts the engine within the Dapr Workflow runtime. +- `await workflowClient.scheduleNewWorkflow(sequence)` schedules a new workflow instance with the Dapr Workflow runtime. +- `await workflowClient.waitForWorkflowCompletion(id, undefined, 30)` waits for the workflow instance to complete. + +In the terminal, execute the following command to kick off the `activity-sequence.ts`: + +```sh +npm run start:dapr:activity-sequence +``` + +**Expected output** + +``` +You're up and running! Both Dapr and your app logs will appear here. + +... + +== APP == Orchestration scheduled with ID: dc040bea-6436-4051-9166-c9294f9d2201 +== APP == Waiting 30 seconds for instance dc040bea-6436-4051-9166-c9294f9d2201 to complete... +== APP == Received "Orchestrator Request" work item with instance id 'dc040bea-6436-4051-9166-c9294f9d2201' +== APP == dc040bea-6436-4051-9166-c9294f9d2201: Rebuilding local state with 0 history event... +== APP == dc040bea-6436-4051-9166-c9294f9d2201: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, EXECUTIONSTARTED=1] +== APP == dc040bea-6436-4051-9166-c9294f9d2201: Waiting for 1 task(s) and 0 event(s) to complete... +== APP == dc040bea-6436-4051-9166-c9294f9d2201: Returning 1 action(s) +== APP == Received "Activity Request" work item +== APP == Activity hello completed with output "Hello Tokyo!" (14 chars) +== APP == Received "Orchestrator Request" work item with instance id 'dc040bea-6436-4051-9166-c9294f9d2201' +== APP == dc040bea-6436-4051-9166-c9294f9d2201: Rebuilding local state with 3 history event... +== APP == dc040bea-6436-4051-9166-c9294f9d2201: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, TASKCOMPLETED=1] +== APP == dc040bea-6436-4051-9166-c9294f9d2201: Waiting for 1 task(s) and 0 event(s) to complete... +== APP == dc040bea-6436-4051-9166-c9294f9d2201: Returning 1 action(s) +== APP == Received "Activity Request" work item +== APP == Activity hello completed with output "Hello Seattle!" (16 chars) +== APP == Received "Orchestrator Request" work item with instance id 'dc040bea-6436-4051-9166-c9294f9d2201' +== APP == dc040bea-6436-4051-9166-c9294f9d2201: Rebuilding local state with 6 history event... +== APP == dc040bea-6436-4051-9166-c9294f9d2201: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, TASKCOMPLETED=1] +== APP == dc040bea-6436-4051-9166-c9294f9d2201: Waiting for 1 task(s) and 0 event(s) to complete... +== APP == dc040bea-6436-4051-9166-c9294f9d2201: Returning 1 action(s) +== APP == Received "Activity Request" work item +== APP == Activity hello completed with output "Hello London!" (15 chars) +== APP == Received "Orchestrator Request" work item with instance id 'dc040bea-6436-4051-9166-c9294f9d2201' +== APP == dc040bea-6436-4051-9166-c9294f9d2201: Rebuilding local state with 9 history event... +== APP == dc040bea-6436-4051-9166-c9294f9d2201: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, TASKCOMPLETED=1] +== APP == dc040bea-6436-4051-9166-c9294f9d2201: Orchestration completed with status COMPLETED +== APP == dc040bea-6436-4051-9166-c9294f9d2201: Returning 1 action(s) +INFO[0006] dc040bea-6436-4051-9166-c9294f9d2201: 'sequence' completed with a COMPLETED status. app_id=activity-sequence-workflow instance=kaibocai-devbox scope=wfengine.backend type=log ver=1.12.3 +== APP == Instance dc040bea-6436-4051-9166-c9294f9d2201 completed +== APP == Orchestration completed! Result: ["Hello Tokyo!","Hello Seattle!","Hello London!"] +``` + +## Next steps + +- [Learn more about Dapr workflow]({{< ref workflow-overview.md >}}) +- [Workflow API reference]({{< ref workflow_api.md >}}) diff --git a/examples/workflow/authoring/README.md b/examples/workflow/authoring/README.md new file mode 100644 index 00000000..4ac79b8c --- /dev/null +++ b/examples/workflow/authoring/README.md @@ -0,0 +1,33 @@ +# Examples - Dapr Workflow + +## Running + +### Activity Sequence example + +```bash +# Install +npm install + +# Run the example +npm run start:dapr:activity-sequence +``` + +### Fan out/Fan in example + +```bash +# Install +npm install + +# Run the example +npm run start:dapr:fanout-fanin +``` + +### Human Interaction in example + +```bash +# Install +npm install + +# Run the example +npm run start:dapr:human-interaction +``` diff --git a/examples/workflow/authoring/components/redis.yaml b/examples/workflow/authoring/components/redis.yaml new file mode 100644 index 00000000..175c0e7a --- /dev/null +++ b/examples/workflow/authoring/components/redis.yaml @@ -0,0 +1,33 @@ +# +# Copyright 2022 The Dapr Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# https://docs.dapr.io/reference/components-reference/supported-bindings/rabbitmq/ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: state-redis + namespace: default +spec: + type: state.redis + version: v1 + metadata: + - name: redisHost + value: localhost:6379 + - name: redisPassword + value: "" + - name: enableTLS + value: "false" + - name: failover + value: "false" + - name: actorStateStore + value: "true" \ No newline at end of file diff --git a/examples/workflow/authoring/package-lock.json b/examples/workflow/authoring/package-lock.json new file mode 100644 index 00000000..ebac4724 --- /dev/null +++ b/examples/workflow/authoring/package-lock.json @@ -0,0 +1,477 @@ +{ + "name": "dapr-example-workflow-authoring", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "dapr-example-workflow-authoring", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@dapr/dapr": "file:../../../build", + "@types/node": "^18.16.3" + }, + "devDependencies": { + "readline-sync": "^1.4.10", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" + } + }, + "../../../build": { + "name": "@dapr/dapr", + "version": "3.2.0", + "license": "ISC", + "dependencies": { + "@grpc/grpc-js": "^1.9.3", + "@js-temporal/polyfill": "^0.3.0", + "@microsoft/durabletask-js": "^0.1.0-alpha.1", + "@types/google-protobuf": "^3.15.5", + "@types/node-fetch": "^2.6.2", + "body-parser": "^1.19.0", + "express": "^4.18.2", + "google-protobuf": "^3.18.0", + "http-terminator": "^3.2.0", + "node-fetch": "^2.6.7" + }, + "devDependencies": { + "@types/body-parser": "^1.19.1", + "@types/express": "^4.17.15", + "@types/jest": "^27.0.1", + "@types/node": "^16.9.1", + "@types/readline-sync": "^1.4.8", + "@types/uuid": "^8.3.1", + "@typescript-eslint/eslint-plugin": "^5.1.0", + "@typescript-eslint/parser": "^5.1.0", + "eslint": "^8.1.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-header": "^3.1.1", + "eslint-plugin-prettier": "^4.2.1", + "grpc_tools_node_protoc_ts": "^5.3.2", + "husky": "^8.0.1", + "jest": "^27.2.0", + "nodemon": "^2.0.20", + "prettier": "^2.4.0", + "pretty-quick": "^3.1.3", + "ts-jest": "^27.0.5", + "ts-node": "^10.9.1", + "typescript": "^4.5.5" + } + }, + "../../build": { + "name": "@dapr/dapr", + "version": "3.0.0", + "extraneous": true, + "license": "ISC", + "dependencies": { + "@grpc/grpc-js": "^1.3.7", + "@js-temporal/polyfill": "^0.3.0", + "@types/google-protobuf": "^3.15.5", + "@types/node-fetch": "^2.6.2", + "body-parser": "^1.19.0", + "express": "^4.18.2", + "google-protobuf": "^3.18.0", + "http-terminator": "^3.0.4", + "node-fetch": "^2.6.7" + }, + "devDependencies": { + "@types/body-parser": "^1.19.1", + "@types/express": "^4.17.15", + "@types/jest": "^27.0.1", + "@types/node": "^16.9.1", + "@types/uuid": "^8.3.1", + "@typescript-eslint/eslint-plugin": "^5.1.0", + "@typescript-eslint/parser": "^5.1.0", + "eslint": "^8.1.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-header": "^3.1.1", + "eslint-plugin-prettier": "^4.2.1", + "grpc_tools_node_protoc_ts": "^5.3.2", + "husky": "^8.0.1", + "jest": "^27.2.0", + "nodemon": "^2.0.20", + "prettier": "^2.4.0", + "pretty-quick": "^3.1.3", + "ts-jest": "^27.0.5", + "typescript": "^4.5.5" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@dapr/dapr": { + "resolved": "../../../build", + "link": true + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.3.tgz", + "integrity": "sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==" + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + }, + "dependencies": { + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, + "@dapr/dapr": { + "version": "file:../../../build", + "requires": { + "@grpc/grpc-js": "^1.9.3", + "@js-temporal/polyfill": "^0.3.0", + "@microsoft/durabletask-js": "^0.1.0-alpha.1", + "@types/body-parser": "^1.19.1", + "@types/express": "^4.17.15", + "@types/google-protobuf": "^3.15.5", + "@types/jest": "^27.0.1", + "@types/node": "^16.9.1", + "@types/node-fetch": "^2.6.2", + "@types/readline-sync": "^1.4.8", + "@types/uuid": "^8.3.1", + "@typescript-eslint/eslint-plugin": "^5.1.0", + "@typescript-eslint/parser": "^5.1.0", + "body-parser": "^1.19.0", + "eslint": "^8.1.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-header": "^3.1.1", + "eslint-plugin-prettier": "^4.2.1", + "express": "^4.18.2", + "google-protobuf": "^3.18.0", + "grpc_tools_node_protoc_ts": "^5.3.2", + "http-terminator": "^3.2.0", + "husky": "^8.0.1", + "jest": "^27.2.0", + "node-fetch": "^2.6.7", + "nodemon": "^2.0.20", + "prettier": "^2.4.0", + "pretty-quick": "^3.1.3", + "ts-jest": "^27.0.5", + "ts-node": "^10.9.1", + "typescript": "^4.5.5" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "@types/node": { + "version": "18.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.3.tgz", + "integrity": "sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==" + }, + "acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", + "dev": true + }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, + "typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + } + } +} diff --git a/examples/workflow/authoring/package.json b/examples/workflow/authoring/package.json new file mode 100644 index 00000000..65e36a5d --- /dev/null +++ b/examples/workflow/authoring/package.json @@ -0,0 +1,26 @@ +{ + "name": "dapr-example-workflow-authoring", + "version": "1.0.0", + "description": "An example utilizing the Dapr JS-SDK to author workflow", + "private": "true", + "scripts": { + "build": "npx tsc --outDir ./dist/", + "start:activity-sequence": "npm run build && node dist/activity-sequence.js", + "start:fanout-fanin": "npm run build && node dist/fanout-fanin.js", + "start:human-interaction": "npm run build && node dist/human-interaction.js", + "start:dapr:activity-sequence": "dapr run --app-id activity-sequence-workflow --app-protocol grpc --dapr-grpc-port 50001 --components-path ./components npm run start:activity-sequence", + "start:dapr:fanout-fanin": "dapr run --app-id activity-sequence-workflow --app-protocol grpc --dapr-grpc-port 50001 --components-path ./components npm run start:fanout-fanin", + "start:dapr:human-interaction": "dapr run --app-id activity-sequence-workflow --app-protocol grpc --dapr-grpc-port 50001 --components-path ./components npm run start:human-interaction" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "readline-sync": "^1.4.10", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" + }, + "dependencies": { + "@dapr/dapr": "file:../../../build", + "@types/node": "^18.16.3" + } +} diff --git a/examples/workflow/authoring/src/activity-sequence.ts b/examples/workflow/authoring/src/activity-sequence.ts new file mode 100644 index 00000000..97d5c8e7 --- /dev/null +++ b/examples/workflow/authoring/src/activity-sequence.ts @@ -0,0 +1,79 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { DaprWorkflowClient, WorkflowActivityContext, WorkflowContext, WorkflowRuntime, TWorkflow } from "@dapr/dapr"; + +async function start() { + // Update the gRPC client and worker to use a local address and port + const daprHost = "localhost"; + const daprPort = "50001"; + const workflowClient = new DaprWorkflowClient({ + daprHost, + daprPort, + }); + const workflowRuntime = new WorkflowRuntime({ + daprHost, + daprPort, + }); + + const hello = async (_: WorkflowActivityContext, name: string) => { + return `Hello ${name}!`; + }; + + const sequence: TWorkflow = async function* (ctx: WorkflowContext): any { + const cities: string[] = []; + + const result1 = yield ctx.callActivity(hello, "Tokyo"); + cities.push(result1); + const result2 = yield ctx.callActivity(hello, "Seattle"); + cities.push(result2); + const result3 = yield ctx.callActivity(hello, "London"); + cities.push(result3); + + return cities; + }; + + workflowRuntime.registerWorkflow(sequence).registerActivity(hello); + + // Wrap the worker startup in a try-catch block to handle any errors during startup + try { + await workflowRuntime.start(); + console.log("Workflow runtime started successfully"); + } catch (error) { + console.error("Error starting workflow runtime:", error); + } + + // Schedule a new orchestration + try { + const id = await workflowClient.scheduleNewWorkflow(sequence); + console.log(`Orchestration scheduled with ID: ${id}`); + + // Wait for orchestration completion + const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30); + + console.log(`Orchestration completed! Result: ${state?.serializedOutput}`); + } catch (error) { + console.error("Error scheduling or waiting for orchestration:", error); + } + + await workflowRuntime.stop(); + await workflowClient.stop(); + + // stop the dapr side car + process.exit(0); +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/examples/workflow/authoring/src/fanout-fanin.ts b/examples/workflow/authoring/src/fanout-fanin.ts new file mode 100644 index 00000000..b74171f0 --- /dev/null +++ b/examples/workflow/authoring/src/fanout-fanin.ts @@ -0,0 +1,113 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { + Task, + DaprWorkflowClient, + WorkflowActivityContext, + WorkflowContext, + WorkflowRuntime, + TWorkflow, +} from "@dapr/dapr"; + +// Wrap the entire code in an immediately-invoked async function +async function start() { + // Update the gRPC client and worker to use a local address and port + const daprHost = "localhost"; + const daprPort = "50001"; + const workflowClient = new DaprWorkflowClient({ + daprHost, + daprPort, + }); + const workflowRuntime = new WorkflowRuntime({ + daprHost, + daprPort, + }); + + function getRandomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + async function getWorkItemsActivity(_: WorkflowActivityContext): Promise { + const count: number = getRandomInt(2, 10); + console.log(`generating ${count} work items...`); + + const workItems: string[] = Array.from({ length: count }, (_, i) => `work item ${i}`); + return workItems; + } + + function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + async function processWorkItemActivity(context: WorkflowActivityContext, item: string): Promise { + console.log(`processing work item: ${item}`); + + // Simulate some work that takes a variable amount of time + const sleepTime = Math.random() * 5000; + await sleep(sleepTime); + + // Return a result for the given work item, which is also a random number in this case + // For more information about random numbers in workflow please check + // https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-code-constraints?tabs=csharp#random-numbers + return Math.floor(Math.random() * 11); + } + + const workflow: TWorkflow = async function* (ctx: WorkflowContext): any { + const tasks: Task[] = []; + const workItems = yield ctx.callActivity(getWorkItemsActivity); + for (const workItem of workItems) { + tasks.push(ctx.callActivity(processWorkItemActivity, workItem)); + } + const results: number[] = yield ctx.whenAll(tasks); + const sum: number = results.reduce((accumulator, currentValue) => accumulator + currentValue, 0); + return sum; + }; + + workflowRuntime.registerWorkflow(workflow); + workflowRuntime.registerActivity(getWorkItemsActivity); + workflowRuntime.registerActivity(processWorkItemActivity); + + // Wrap the worker startup in a try-catch block to handle any errors during startup + try { + await workflowRuntime.start(); + console.log("Worker started successfully"); + } catch (error) { + console.error("Error starting worker:", error); + } + + // Schedule a new orchestration + try { + const id = await workflowClient.scheduleNewWorkflow(workflow); + console.log(`Orchestration scheduled with ID: ${id}`); + + // Wait for orchestration completion + const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30); + + console.log(`Orchestration completed! Result: ${state?.serializedOutput}`); + } catch (error) { + console.error("Error scheduling or waiting for orchestration:", error); + } + + // stop worker and client + await workflowRuntime.stop(); + await workflowClient.stop(); + + // stop the dapr side car + process.exit(0); +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/examples/workflow/authoring/src/human-interaction.ts b/examples/workflow/authoring/src/human-interaction.ts new file mode 100644 index 00000000..bff52cc8 --- /dev/null +++ b/examples/workflow/authoring/src/human-interaction.ts @@ -0,0 +1,145 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { + Task, + DaprWorkflowClient, + WorkflowActivityContext, + WorkflowContext, + WorkflowRuntime, + TWorkflow, +} from "@dapr/dapr"; +import * as readlineSync from "readline-sync"; + +// Wrap the entire code in an immediately-invoked async function +async function start() { + class Order { + cost: number; + product: string; + quantity: number; + constructor(cost: number, product: string, quantity: number) { + this.cost = cost; + this.product = product; + this.quantity = quantity; + } + } + + function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + // Update the gRPC client and worker to use a local address and port + const daprHost = "localhost"; + const daprPort = "50001"; + const workflowClient = new DaprWorkflowClient({ + daprHost, + daprPort, + }); + const workflowRuntime = new WorkflowRuntime({ + daprHost, + daprPort, + }); + + // Activity function that sends an approval request to the manager + const sendApprovalRequest = async (_: WorkflowActivityContext, order: Order) => { + // Simulate some work that takes an amount of time + await sleep(3000); + console.log(`Sending approval request for order: ${order.product}`); + }; + + // Activity function that places an order + const placeOrder = async (_: WorkflowActivityContext, order: Order) => { + console.log(`Placing order: ${order.product}`); + }; + + // Orchestrator function that represents a purchase order workflow + const purchaseOrderWorkflow: TWorkflow = async function* (ctx: WorkflowContext, order: Order): any { + // Orders under $1000 are auto-approved + if (order.cost < 1000) { + return "Auto-approved"; + } + + // Orders of $1000 or more require manager approval + yield ctx.callActivity(sendApprovalRequest, order); + + // Approvals must be received within 24 hours or they will be cancled. + const tasks: Task[] = []; + const approvalEvent = ctx.waitForExternalEvent("approval_received"); + const timeoutEvent = ctx.createTimer(24 * 60 * 60); + tasks.push(approvalEvent); + tasks.push(timeoutEvent); + const winner = ctx.whenAny(tasks); + + if (winner == timeoutEvent) { + return "Cancelled"; + } + + yield ctx.callActivity(placeOrder, order); + const approvalDetails = approvalEvent.getResult(); + return `Approved by ${approvalDetails.approver}`; + }; + + workflowRuntime + .registerWorkflow(purchaseOrderWorkflow) + .registerActivity(sendApprovalRequest) + .registerActivity(placeOrder); + + // Wrap the worker startup in a try-catch block to handle any errors during startup + try { + await workflowRuntime.start(); + console.log("Worker started successfully"); + } catch (error) { + console.error("Error starting worker:", error); + } + + // Schedule a new orchestration + try { + const cost = readlineSync.questionInt("Cost of your order:"); + const approver = readlineSync.question("Approver of your order:"); + const timeout = readlineSync.questionInt("Timeout for your order in seconds:"); + const order = new Order(cost, "MyProduct", 1); + const id = await workflowClient.scheduleNewWorkflow(purchaseOrderWorkflow, order); + console.log(`Orchestration scheduled with ID: ${id}`); + + // prompt for approval asynchronously + promptForApproval(approver, workflowClient, id); + + // Wait for orchestration completion + const state = await workflowClient.waitForWorkflowCompletion(id, undefined, timeout + 2); + + console.log(`Orchestration completed! Result: ${state?.serializedOutput}`); + } catch (error) { + console.error("Error scheduling or waiting for orchestration:", error); + } + + // stop worker and client + await workflowRuntime.stop(); + await workflowClient.stop(); + + // stop the dapr side car + process.exit(0); +} + +async function promptForApproval(approver: string, workflowClient: DaprWorkflowClient, id: string) { + if (readlineSync.keyInYN("Press [Y] to approve the order... Y/yes, N/no")) { + const approvalEvent = { approver: approver }; + await workflowClient.raiseEvent(id, "approval_received", approvalEvent); + } else { + return "Order rejected"; + } +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/examples/workflow/authoring/tsconfig.json b/examples/workflow/authoring/tsconfig.json new file mode 100644 index 00000000..fbbfbe8d --- /dev/null +++ b/examples/workflow/authoring/tsconfig.json @@ -0,0 +1,71 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./dist" /* Redirect output structure to the directory. */, + "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/examples/workflow/package-lock.json b/examples/workflow/management/package-lock.json similarity index 89% rename from examples/workflow/package-lock.json rename to examples/workflow/management/package-lock.json index 60d31df1..c2a8b93c 100644 --- a/examples/workflow/package-lock.json +++ b/examples/workflow/management/package-lock.json @@ -1,15 +1,15 @@ { - "name": "dapr-example-workflow", + "name": "dapr-example-workflow-management", "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "dapr-example-workflow", + "name": "dapr-example-workflow-management", "version": "1.0.0", "license": "ISC", "dependencies": { - "@dapr/dapr": "file:../../build", + "@dapr/dapr": "file:../../../build", "@types/node": "^18.16.3" }, "devDependencies": { @@ -17,9 +17,52 @@ "typescript": "^5.0.4" } }, + "../../../build": { + "name": "@dapr/dapr", + "version": "3.2.0", + "license": "ISC", + "dependencies": { + "@grpc/grpc-js": "^1.9.3", + "@js-temporal/polyfill": "^0.3.0", + "@microsoft/durabletask-js": "^0.1.0-alpha.1", + "@types/google-protobuf": "^3.15.5", + "@types/node-fetch": "^2.6.2", + "body-parser": "^1.19.0", + "express": "^4.18.2", + "google-protobuf": "^3.18.0", + "http-terminator": "^3.2.0", + "node-fetch": "^2.6.7" + }, + "devDependencies": { + "@swc/core": "^1.3.55", + "@types/body-parser": "^1.19.1", + "@types/express": "^4.17.15", + "@types/jest": "^27.0.1", + "@types/node": "^16.9.1", + "@types/readline-sync": "^1.4.8", + "@types/uuid": "^8.3.1", + "@typescript-eslint/eslint-plugin": "^5.1.0", + "@typescript-eslint/parser": "^5.1.0", + "eslint": "^8.1.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-header": "^3.1.1", + "eslint-plugin-prettier": "^4.2.1", + "grpc_tools_node_protoc_ts": "^5.3.2", + "husky": "^8.0.1", + "jest": "^27.2.0", + "nodemon": "^2.0.20", + "prettier": "^2.4.0", + "pretty-quick": "^3.1.3", + "readline-sync": "^1.4.10", + "ts-jest": "^27.0.5", + "ts-node": "^10.9.1", + "typescript": "^4.5.5" + } + }, "../../build": { "name": "@dapr/dapr", "version": "3.0.0", + "extraneous": true, "license": "ISC", "dependencies": { "@grpc/grpc-js": "^1.3.7", @@ -67,7 +110,7 @@ } }, "node_modules/@dapr/dapr": { - "resolved": "../../build", + "resolved": "../../../build", "link": true }, "node_modules/@jridgewell/resolve-uri": { @@ -255,16 +298,19 @@ } }, "@dapr/dapr": { - "version": "file:../../build", + "version": "file:../../../build", "requires": { - "@grpc/grpc-js": "^1.3.7", + "@grpc/grpc-js": "^1.9.3", "@js-temporal/polyfill": "^0.3.0", + "@microsoft/durabletask-js": "^0.1.0-alpha.1", + "@swc/core": "^1.3.55", "@types/body-parser": "^1.19.1", "@types/express": "^4.17.15", "@types/google-protobuf": "^3.15.5", "@types/jest": "^27.0.1", "@types/node": "^16.9.1", "@types/node-fetch": "^2.6.2", + "@types/readline-sync": "^1.4.8", "@types/uuid": "^8.3.1", "@typescript-eslint/eslint-plugin": "^5.1.0", "@typescript-eslint/parser": "^5.1.0", @@ -276,14 +322,16 @@ "express": "^4.18.2", "google-protobuf": "^3.18.0", "grpc_tools_node_protoc_ts": "^5.3.2", - "http-terminator": "^3.0.4", + "http-terminator": "^3.2.0", "husky": "^8.0.1", "jest": "^27.2.0", "node-fetch": "^2.6.7", "nodemon": "^2.0.20", "prettier": "^2.4.0", "pretty-quick": "^3.1.3", + "readline-sync": "^1.4.10", "ts-jest": "^27.0.5", + "ts-node": "^10.9.1", "typescript": "^4.5.5" } }, diff --git a/examples/workflow/package.json b/examples/workflow/management/package.json similarity index 89% rename from examples/workflow/package.json rename to examples/workflow/management/package.json index 46fc5e1c..e11854cc 100644 --- a/examples/workflow/package.json +++ b/examples/workflow/management/package.json @@ -1,5 +1,5 @@ { - "name": "dapr-example-workflow", + "name": "dapr-example-workflow-management", "version": "1.0.0", "description": "An example utilizing the Dapr JS-SDK to manage workflow", "main": "dist/index.js", @@ -17,7 +17,7 @@ "typescript": "^5.0.4" }, "dependencies": { - "@dapr/dapr": "file:../../build", + "@dapr/dapr": "file:../../../build", "@types/node": "^18.16.3" } } diff --git a/examples/workflow/src/index.ts b/examples/workflow/management/src/index.ts similarity index 100% rename from examples/workflow/src/index.ts rename to examples/workflow/management/src/index.ts diff --git a/examples/workflow/tsconfig.json b/examples/workflow/management/tsconfig.json similarity index 100% rename from examples/workflow/tsconfig.json rename to examples/workflow/management/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 7666e0b4..beae1bce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@grpc/grpc-js": "^1.9.3", "@js-temporal/polyfill": "^0.3.0", + "@microsoft/durabletask-js": "^0.1.0-alpha.1", "@types/google-protobuf": "^3.15.5", "@types/node-fetch": "^2.6.2", "body-parser": "^1.19.0", @@ -703,6 +704,32 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1234,6 +1261,15 @@ "node": ">=12" } }, + "node_modules/@microsoft/durabletask-js": { + "version": "0.1.0-alpha.1", + "resolved": "https://registry.npmjs.org/@microsoft/durabletask-js/-/durabletask-js-0.1.0-alpha.1.tgz", + "integrity": "sha512-wdBCz86FCj2lknLqyjU+J0Auetxxr7vj0SUjPjnjxRaE0VrM4G3WmX65XDlsligoIg2JEe0M89REzaA6IVh4pw==", + "dependencies": { + "@grpc/grpc-js": "^1.8.14", + "google-protobuf": "^3.21.2" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1341,6 +1377,232 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/@swc/core": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.101.tgz", + "integrity": "sha512-w5aQ9qYsd/IYmXADAnkXPGDMTqkQalIi+kfFf/MHRKTpaOL7DHjMXwPp/n8hJ0qNjRvchzmPtOqtPBiER50d8A==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "@swc/counter": "^0.1.1", + "@swc/types": "^0.1.5" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.3.101", + "@swc/core-darwin-x64": "1.3.101", + "@swc/core-linux-arm-gnueabihf": "1.3.101", + "@swc/core-linux-arm64-gnu": "1.3.101", + "@swc/core-linux-arm64-musl": "1.3.101", + "@swc/core-linux-x64-gnu": "1.3.101", + "@swc/core-linux-x64-musl": "1.3.101", + "@swc/core-win32-arm64-msvc": "1.3.101", + "@swc/core-win32-ia32-msvc": "1.3.101", + "@swc/core-win32-x64-msvc": "1.3.101" + }, + "peerDependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.101.tgz", + "integrity": "sha512-mNFK+uHNPRXSnfTOG34zJOeMl2waM4hF4a2NY7dkMXrPqw9CoJn4MwTXJcyMiSz1/BnNjjTCHF3Yhj0jPxmkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.101.tgz", + "integrity": "sha512-B085j8XOx73Fg15KsHvzYWG262bRweGr3JooO1aW5ec5pYbz5Ew9VS5JKYS03w2UBSxf2maWdbPz2UFAxg0whw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.101.tgz", + "integrity": "sha512-9xLKRb6zSzRGPqdz52Hy5GuB1lSjmLqa0lST6MTFads3apmx4Vgs8Y5NuGhx/h2I8QM4jXdLbpqQlifpzTlSSw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.101.tgz", + "integrity": "sha512-oE+r1lo7g/vs96Weh2R5l971dt+ZLuhaUX+n3BfDdPxNHfObXgKMjO7E+QS5RbGjv/AwiPCxQmbdCp/xN5ICJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.101.tgz", + "integrity": "sha512-OGjYG3H4BMOTnJWJyBIovCez6KiHF30zMIu4+lGJTCrxRI2fAjGLml3PEXj8tC3FMcud7U2WUn6TdG0/te2k6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.101.tgz", + "integrity": "sha512-/kBMcoF12PRO/lwa8Z7w4YyiKDcXQEiLvM+S3G9EvkoKYGgkkz4Q6PSNhF5rwg/E3+Hq5/9D2R+6nrkF287ihg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.101.tgz", + "integrity": "sha512-kDN8lm4Eew0u1p+h1l3JzoeGgZPQ05qDE0czngnjmfpsH2sOZxVj1hdiCwS5lArpy7ktaLu5JdRnx70MkUzhXw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.101.tgz", + "integrity": "sha512-9Wn8TTLWwJKw63K/S+jjrZb9yoJfJwCE2RV5vPCCWmlMf3U1AXj5XuWOLUX+Rp2sGKau7wZKsvywhheWm+qndQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.101.tgz", + "integrity": "sha512-onO5KvICRVlu2xmr4//V2je9O2XgS1SGKpbX206KmmjcJhXN5EYLSxW9qgg+kgV5mip+sKTHTAu7IkzkAtElYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.101.tgz", + "integrity": "sha512-T3GeJtNQV00YmiVw/88/nxJ/H43CJvFnpvBHCVn17xbahiVUOPOduh3rc9LgAkKiNt/aV8vU3OJR+6PhfMR7UQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", + "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@swc/types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", + "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -1350,6 +1612,38 @@ "node": ">= 6" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.2", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.2.tgz", @@ -1982,6 +2276,14 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2511,6 +2813,14 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2681,6 +2991,17 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", @@ -6721,6 +7042,62 @@ } } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/acorn-walk": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -6906,6 +7283,14 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/v8-to-istanbul": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", @@ -7142,6 +7527,17 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -7662,6 +8058,31 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -8070,6 +8491,15 @@ "tslib": "^2.3.1" } }, + "@microsoft/durabletask-js": { + "version": "0.1.0-alpha.1", + "resolved": "https://registry.npmjs.org/@microsoft/durabletask-js/-/durabletask-js-0.1.0-alpha.1.tgz", + "integrity": "sha512-wdBCz86FCj2lknLqyjU+J0Auetxxr7vj0SUjPjnjxRaE0VrM4G3WmX65XDlsligoIg2JEe0M89REzaA6IVh4pw==", + "requires": { + "@grpc/grpc-js": "^1.8.14", + "google-protobuf": "^3.21.2" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -8168,12 +8598,162 @@ "@sinonjs/commons": "^1.7.0" } }, + "@swc/core": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.101.tgz", + "integrity": "sha512-w5aQ9qYsd/IYmXADAnkXPGDMTqkQalIi+kfFf/MHRKTpaOL7DHjMXwPp/n8hJ0qNjRvchzmPtOqtPBiER50d8A==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@swc/core-darwin-arm64": "1.3.101", + "@swc/core-darwin-x64": "1.3.101", + "@swc/core-linux-arm-gnueabihf": "1.3.101", + "@swc/core-linux-arm64-gnu": "1.3.101", + "@swc/core-linux-arm64-musl": "1.3.101", + "@swc/core-linux-x64-gnu": "1.3.101", + "@swc/core-linux-x64-musl": "1.3.101", + "@swc/core-win32-arm64-msvc": "1.3.101", + "@swc/core-win32-ia32-msvc": "1.3.101", + "@swc/core-win32-x64-msvc": "1.3.101", + "@swc/counter": "^0.1.1", + "@swc/types": "^0.1.5" + } + }, + "@swc/core-darwin-arm64": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.101.tgz", + "integrity": "sha512-mNFK+uHNPRXSnfTOG34zJOeMl2waM4hF4a2NY7dkMXrPqw9CoJn4MwTXJcyMiSz1/BnNjjTCHF3Yhj0jPxmkzQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/core-darwin-x64": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.101.tgz", + "integrity": "sha512-B085j8XOx73Fg15KsHvzYWG262bRweGr3JooO1aW5ec5pYbz5Ew9VS5JKYS03w2UBSxf2maWdbPz2UFAxg0whw==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/core-linux-arm-gnueabihf": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.101.tgz", + "integrity": "sha512-9xLKRb6zSzRGPqdz52Hy5GuB1lSjmLqa0lST6MTFads3apmx4Vgs8Y5NuGhx/h2I8QM4jXdLbpqQlifpzTlSSw==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/core-linux-arm64-gnu": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.101.tgz", + "integrity": "sha512-oE+r1lo7g/vs96Weh2R5l971dt+ZLuhaUX+n3BfDdPxNHfObXgKMjO7E+QS5RbGjv/AwiPCxQmbdCp/xN5ICJA==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/core-linux-arm64-musl": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.101.tgz", + "integrity": "sha512-OGjYG3H4BMOTnJWJyBIovCez6KiHF30zMIu4+lGJTCrxRI2fAjGLml3PEXj8tC3FMcud7U2WUn6TdG0/te2k6g==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/core-linux-x64-gnu": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.101.tgz", + "integrity": "sha512-/kBMcoF12PRO/lwa8Z7w4YyiKDcXQEiLvM+S3G9EvkoKYGgkkz4Q6PSNhF5rwg/E3+Hq5/9D2R+6nrkF287ihg==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/core-linux-x64-musl": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.101.tgz", + "integrity": "sha512-kDN8lm4Eew0u1p+h1l3JzoeGgZPQ05qDE0czngnjmfpsH2sOZxVj1hdiCwS5lArpy7ktaLu5JdRnx70MkUzhXw==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/core-win32-arm64-msvc": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.101.tgz", + "integrity": "sha512-9Wn8TTLWwJKw63K/S+jjrZb9yoJfJwCE2RV5vPCCWmlMf3U1AXj5XuWOLUX+Rp2sGKau7wZKsvywhheWm+qndQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/core-win32-ia32-msvc": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.101.tgz", + "integrity": "sha512-onO5KvICRVlu2xmr4//V2je9O2XgS1SGKpbX206KmmjcJhXN5EYLSxW9qgg+kgV5mip+sKTHTAu7IkzkAtElYA==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/core-win32-x64-msvc": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.101.tgz", + "integrity": "sha512-T3GeJtNQV00YmiVw/88/nxJ/H43CJvFnpvBHCVn17xbahiVUOPOduh3rc9LgAkKiNt/aV8vU3OJR+6PhfMR7UQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/counter": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", + "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", + "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", + "dev": true, + "optional": true, + "peer": true + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true, + "optional": true, + "peer": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "optional": true, + "peer": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "optional": true, + "peer": true + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "optional": true, + "peer": true + }, "@types/babel__core": { "version": "7.20.2", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.2.tgz", @@ -8660,6 +9240,14 @@ "picomatch": "^2.0.4" } }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "optional": true, + "peer": true + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -9047,6 +9635,14 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "optional": true, + "peer": true + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -9171,6 +9767,14 @@ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "optional": true, + "peer": true + }, "diff-sequences": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", @@ -12201,6 +12805,39 @@ "yargs-parser": "20.x" } }, + "ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "dependencies": { + "acorn-walk": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", + "dev": true, + "optional": true, + "peer": true + } + } + }, "tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -12325,6 +12962,14 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "optional": true, + "peer": true + }, "v8-to-istanbul": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", @@ -12508,6 +13153,14 @@ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "optional": true, + "peer": true + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index e7167ffe..94880528 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test:load": "jest --runInBand --detectOpenHandles", "test:load:http": "TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol http --app-port 50001 --dapr-http-port 50000 --components-path ./test/components -- npm run test:load 'test/load'", "test:e2e": "jest --runInBand --detectOpenHandles", - "test:e2e:all": "npm run test:e2e:http; npm run test:e2e:grpc; npm run test:e2e:common", + "test:e2e:all": "npm run test:e2e:http; npm run test:e2e:grpc; npm run test:e2e:common; npm run test:e2e:workflow", "test:e2e:grpc": "npm run test:e2e:grpc:client && npm run test:e2e:grpc:server && npm run test:e2e:grpc:clientWithApiToken", "test:e2e:grpc:client": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol grpc --app-port 50001 --dapr-grpc-port 50000 --components-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/grpc/*client.test.ts' ]", "test:e2e:grpc:clientWithApiToken": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 DAPR_API_TOKEN=test dapr run --app-id test-suite --app-protocol grpc --app-port 50001 --dapr-grpc-port 50000 --components-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/grpc/clientWithApiToken.test.ts' ]", @@ -20,6 +20,9 @@ "test:e2e:common": "npm run test:e2e:common:client && npm run test:e2e:common:server", "test:e2e:common:client": "./scripts/test-e2e-common.sh client", "test:e2e:common:server": "./scripts/test-e2e-common.sh server", + "test:e2e:workflow": "npm run prebuild && dapr run --app-id workflow-test-suite --app-protocol grpc --dapr-grpc-port 4001 --components-path ./test/components/workflow -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/workflow/workflow.test.ts' ]", + "test:e2e:workflow:internal": "jest test/e2e/workflow --runInBand --detectOpenHandles", + "test:e2e:workflow:durabletask": "./scripts/test-e2e-workflow.sh", "test:unit": "jest --runInBand --detectOpenHandles", "test:unit:all": "npm run test:unit:main && npm run test:unit:http && npm run test:unit:grpc && npm run test:unit:common && npm run test:unit:actors && npm run test:unit:logger && npm run test:unit:utils && npm run test:unit:errors", "test:unit:main": "NODE_ENV=test npm run test:unit 'test/unit/main/.*\\.test\\.ts'", @@ -30,6 +33,7 @@ "test:unit:logger": "NODE_ENV=test npm run test:unit 'test/unit/logger/.*\\.test\\.ts'", "test:unit:utils": "NODE_ENV=test npm run test:unit 'test/unit/utils/.*\\.test\\.ts'", "test:unit:errors": "NODE_ENV=test npm run test:unit 'test/unit/errors/.*\\.test\\.ts'", + "test:unit:workflow": "NODE_ENV=test npm run test:unit 'test/unit/workflow/.*\\.test\\.ts'", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", "prebuild": "./scripts/prebuild.sh", "build-ci": "npm run prebuild && ./scripts/build.sh", @@ -50,6 +54,7 @@ "express": "^4.18.2", "google-protobuf": "^3.18.0", "http-terminator": "^3.2.0", + "@microsoft/durabletask-js": "^0.1.0-alpha.1", "node-fetch": "^2.6.7" }, "devDependencies": { diff --git a/scripts/test-e2e-workflow.sh b/scripts/test-e2e-workflow.sh new file mode 100755 index 00000000..18372c93 --- /dev/null +++ b/scripts/test-e2e-workflow.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Start the sidecar if it is not running yet +if [ ! "$(docker ps -q -f name=durabletask-sidecar)" ]; then + if [ "$(docker ps -aq -f status=exited -f name=durabletask-sidecar)" ]; then + # cleanup + docker rm durabletask-sidecar + fi + + # run your container + echo "Starting Sidecar" + docker run \ + --name durabletask-sidecar -d --rm \ + -p 4001:4001 \ + --env 'DURABLETASK_SIDECAR_LOGLEVEL=Debug' \ + cgillum/durabletask-sidecar:latest start \ + --backend Emulator +fi + +echo "Running workflow E2E tests" +npm run test:e2e:workflow:internal + +# It should fail if the npm run fails +if [ $? -ne 0 ]; then + echo "E2E tests failed" + exit 1 +fi + +echo "Stopping Sidecar" +docker stop durabletask-sidecar \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index aaee2a0d..8a0a929d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,6 +40,20 @@ import StateConcurrencyEnum from "./enum/StateConcurrency.enum"; import StateConsistencyEnum from "./enum/StateConsistency.enum"; import { StateGetBulkOptions } from "./types/state/StateGetBulkOptions.type"; +import DaprWorkflowClient from "./workflow/client/DaprWorkflowClient"; +import WorkflowActivityContext from "./workflow/runtime/WorkflowActivityContext"; +import WorkflowContext from "./workflow/runtime/WorkflowContext"; +import WorkflowRuntime from "./workflow/runtime/WorkflowRuntime"; +import { TWorkflow } from "./types/workflow/Workflow.type"; +import { Task } from "@microsoft/durabletask-js/task/task"; +import { WorkflowFailureDetails } from "./workflow/client/WorkflowFailureDetails"; +import { WorkflowState } from "./workflow/client/WorkflowState"; +import { + WorkflowRuntimeStatus, + fromOrchestrationStatus, + toOrchestrationStatus, +} from "./workflow/runtime/WorkflowRuntimeStatus"; + export { DaprClient, DaprServer, @@ -65,4 +79,15 @@ export { StateConsistencyEnum, PubSubBulkPublishResponse, StateGetBulkOptions, + DaprWorkflowClient, + WorkflowActivityContext, + WorkflowContext, + WorkflowRuntime, + TWorkflow, + Task, + WorkflowFailureDetails, + WorkflowState, + WorkflowRuntimeStatus, + fromOrchestrationStatus, + toOrchestrationStatus, }; diff --git a/src/types/workflow/Activity.type.ts b/src/types/workflow/Activity.type.ts new file mode 100644 index 00000000..37db69b7 --- /dev/null +++ b/src/types/workflow/Activity.type.ts @@ -0,0 +1,19 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import WorkflowActivityContext from "../../workflow/runtime/WorkflowActivityContext"; + +/** + * The type of the activity function. + */ +export type TWorkflowActivity = (context: WorkflowActivityContext, input: TInput) => TOutput; diff --git a/src/types/workflow/InputOutput.type.ts b/src/types/workflow/InputOutput.type.ts new file mode 100644 index 00000000..ae1f4d80 --- /dev/null +++ b/src/types/workflow/InputOutput.type.ts @@ -0,0 +1,22 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * The type of the input for the workflow activity + */ +export type TInput = any; + +/** + * The type of the output for the workflow and workflow activity + */ +export type TOutput = any; diff --git a/src/types/workflow/Workflow.type.ts b/src/types/workflow/Workflow.type.ts new file mode 100644 index 00000000..36d2bbc2 --- /dev/null +++ b/src/types/workflow/Workflow.type.ts @@ -0,0 +1,21 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import WorkflowContext from "../../workflow/runtime/WorkflowContext"; +import { Task } from "@microsoft/durabletask-js/task/task"; +import { TOutput } from "./InputOutput.type"; + +/** + * The type of the workflow. + */ +export type TWorkflow = (context: WorkflowContext, input: any) => Generator, any, any> | TOutput; diff --git a/src/types/workflow/WorkflowClientOption.ts b/src/types/workflow/WorkflowClientOption.ts new file mode 100644 index 00000000..6b7f98d0 --- /dev/null +++ b/src/types/workflow/WorkflowClientOption.ts @@ -0,0 +1,45 @@ +/* +Copyright 2022 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as grpc from "@grpc/grpc-js"; +import { LoggerOptions } from "../logger/LoggerOptions"; + +export type WorkflowClientOptions = { + /** + * Host location of the Dapr sidecar. + * Default is 127.0.0.1. + */ + daprHost: string; + + /** + * Port of the Dapr sidecar running a gRPC server. + * Default is 50001. + */ + daprPort: string; + + /** + * Options related to logging. + */ + logger?: LoggerOptions; + + /** + * API token to authenticate with Dapr. + * See https://docs.dapr.io/operations/security/api-token/. + */ + daprApiToken?: string; + + /** + * options used when initializing a grpc Channel instance. + */ + grpcOptions?: grpc.ChannelOptions; +}; diff --git a/src/workflow/client/DaprWorkflowClient.ts b/src/workflow/client/DaprWorkflowClient.ts new file mode 100644 index 00000000..8b761610 --- /dev/null +++ b/src/workflow/client/DaprWorkflowClient.ts @@ -0,0 +1,201 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { TaskHubGrpcClient } from "@microsoft/durabletask-js"; +import { WorkflowState } from "./WorkflowState"; +import { generateApiTokenClientInterceptors, generateEndpoint, getDaprApiToken } from "../internal/index"; +import { TWorkflow } from "../../types/workflow/Workflow.type"; +import { getFunctionName } from "../internal"; +import { WorkflowClientOptions } from "../../types/workflow/WorkflowClientOption"; + +/** + * Class that defines client operations for managing workflow instances. + * + * Instances of this class can be used to start, query, raise events to, and terminate workflow instances. In most + * cases, methods on this class accept an instance ID as a parameter, which identifies the workflow instance. + */ +export default class DaprWorkflowClient { + private readonly _innerClient: TaskHubGrpcClient; + + /** + * Initializes a new instance of the DaprWorkflowClient. + * @param {WorkflowClientOptions | undefined} options - Additional options for configuring DaprWorkflowClient. + */ + constructor(options: Partial = {}) { + const grpcEndpoint = generateEndpoint(options); + options.daprApiToken = getDaprApiToken(options); + this._innerClient = this.buildInnerClient(grpcEndpoint.endpoint, options); + } + + private buildInnerClient(hostAddress: string, options: Partial): TaskHubGrpcClient { + let innerOptions = options?.grpcOptions; + if (options.daprApiToken !== undefined && options.daprApiToken !== "") { + innerOptions = { + ...innerOptions, + interceptors: [generateApiTokenClientInterceptors(options), ...(innerOptions?.interceptors ?? [])], + }; + } + return new TaskHubGrpcClient(hostAddress, innerOptions); + } + + /** + * Schedules a new workflow using the DurableTask client. + * + * @param {TWorkflow | string} workflow - The Workflow or the name of the workflow to be scheduled. + * @param {any} [input] - The input to be provided to the scheduled workflow. + * @param {string} [instanceId] - An optional unique identifier for the workflow instance. + * @param {Date} [startAt] - An optional date and time at which the workflow should start. + * @return {Promise} A Promise resolving to the unique ID of the scheduled workflow instance. + */ + public async scheduleNewWorkflow( + workflow: TWorkflow | string, + input?: any, + instanceId?: string, + startAt?: Date, + ): Promise { + if (typeof workflow === "string") { + return await this._innerClient.scheduleNewOrchestration(workflow, input, instanceId, startAt); + } + return await this._innerClient.scheduleNewOrchestration(getFunctionName(workflow), input, instanceId, startAt); + } + + /** + * Terminates the workflow associated with the provided instance id. + * + * @param {string} workflowInstanceId - Workflow instance id to terminate. + * @param {any} output - The optional output to set for the terminated workflow instance. + */ + public async terminateWorkflow(workflowInstanceId: string, output: any) { + await this._innerClient.terminateOrchestration(workflowInstanceId, output); + } + + /** + * Fetches workflow instance metadata from the configured durable store. + * + * @param {string} workflowInstanceId - The unique identifier of the workflow instance to fetch. + * @param {boolean} getInputsAndOutputs - Indicates whether to fetch the workflow instance's + * inputs, outputs, and custom status (true) or omit them (false). + * @returns {Promise} A Promise that resolves to a metadata record describing + * the workflow instance and its execution status, or undefined + * if the instance is not found. + */ + public async getWorkflowState( + workflowInstanceId: string, + getInputsAndOutputs: boolean, + ): Promise { + const state = await this._innerClient.getOrchestrationState(workflowInstanceId, getInputsAndOutputs); + if (state !== undefined) { + return new WorkflowState(state); + } + } + + /** + * Waits for a workflow to start running and returns a {@link WorkflowState} object + * containing metadata about the started instance, and optionally, its input, output, + * and custom status payloads. + * + * A "started" workflow instance refers to any instance not in the Pending state. + * + * If a workflow instance is already running when this method is called, it returns immediately. + * + * @param {string} workflowInstanceId - The unique identifier of the workflow instance to wait for. + * @param {boolean} fetchPayloads - Indicates whether to fetch the workflow instance's + * inputs, outputs (true) or omit them (false). + * @param {number} timeoutInSeconds - The amount of time, in seconds, to wait for the workflow instance to start. + * @returns {Promise} A Promise that resolves to the workflow instance metadata + * or undefined if no such instance is found. + */ + public async waitForWorkflowStart( + workflowInstanceId: string, + fetchPayloads = true, + timeoutInSeconds = 60, + ): Promise { + const state = await this._innerClient.waitForOrchestrationStart( + workflowInstanceId, + fetchPayloads, + timeoutInSeconds, + ); + if (state !== undefined) { + return new WorkflowState(state); + } + } + + /** + * Waits for a workflow to complete running and returns a {@link WorkflowState} object + * containing metadata about the completed instance, and optionally, its input, output, + * and custom status payloads. + * + * A "completed" workflow instance refers to any instance in one of the terminal states. + * For example, the Completed, Failed, or Terminated states. + * + * If a workflow instance is already running when this method is called, it returns immediately. + * + * @param {string} workflowInstanceId - The unique identifier of the workflow instance to wait for. + * @param {boolean} fetchPayloads - Indicates whether to fetch the workflow instance's + * inputs, outputs (true) or omit them (false). + * @param {number} timeoutInSeconds - The amount of time, in seconds, to wait for the workflow instance to complete. Defaults to 60 seconds. + * @returns {Promise} A Promise that resolves to the workflow instance metadata + * or undefined if no such instance is found. + */ + public async waitForWorkflowCompletion( + workflowInstanceId: string, + fetchPayloads = true, + timeoutInSeconds = 60, + ): Promise { + const state = await this._innerClient.waitForOrchestrationCompletion( + workflowInstanceId, + fetchPayloads, + timeoutInSeconds, + ); + if (state != undefined) { + return new WorkflowState(state); + } + } + + /** + * Sends an event notification message to an awaiting workflow instance. + * + * This method triggers the specified event in a running workflow instance, + * allowing the workflow to respond to the event if it has defined event handlers. + * + * @param {string} workflowInstanceId - The unique identifier of the workflow instance that will handle the event. + * @param {string} eventName - The name of the event. Event names are case-insensitive. + * @param {any} [eventPayload] - An optional serializable data payload to include with the event. + */ + public async raiseEvent(workflowInstanceId: string, eventName: string, eventPayload?: any) { + this._innerClient.raiseOrchestrationEvent(workflowInstanceId, eventName, eventPayload); + } + + /** + * Purges the workflow instance state from the workflow state store. + * + * This method removes the persisted state associated with a workflow instance from the state store. + * + * @param {string} workflowInstanceId - The unique identifier of the workflow instance to purge. + * @return {Promise} A Promise that resolves to true if the workflow state was found and purged successfully, otherwise false. + */ + public async purgeWorkflow(workflowInstanceId: string): Promise { + const purgeResult = await this._innerClient.purgeOrchestration(workflowInstanceId); + if (purgeResult !== undefined) { + return purgeResult.deletedInstanceCount > 0; + } + return false; + } + + /** + * Closes the inner DurableTask client and shutdown the GRPC channel. + */ + public async stop() { + await this._innerClient.stop(); + } +} diff --git a/src/workflow/client/WorkflowFailureDetails.ts b/src/workflow/client/WorkflowFailureDetails.ts new file mode 100644 index 00000000..240fbc2a --- /dev/null +++ b/src/workflow/client/WorkflowFailureDetails.ts @@ -0,0 +1,53 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { FailureDetails } from "@microsoft/durabletask-js/task/failure-details"; + +/** + * Class that represents the details of a task failure. + * + * In most cases, failures are caused by unhandled exceptions in activity or workflow code, in which case instances + * of this class will expose the details of the exception. However, it's also possible that other types of errors could + * result in task failures, in which case there may not be any exception-specific information. + */ +export class WorkflowFailureDetails { + private readonly failureDetails: FailureDetails; + + constructor(failureDetails: FailureDetails) { + this.failureDetails = failureDetails; + } + + /** + * Gets the error type, which is the namespace-qualified exception type name. + * @return {string} The error type. + */ + public getErrorType(): string { + return this.failureDetails.errorType; + } + + /** + * Gets the error message. + * @return {string} The error message. + */ + public getErrorMessage(): string { + return this.failureDetails.message; + } + + /** + * Gets the stack trace. + * @return {string | undefined} The stack trace, or undefined if not available. + */ + public getStackTrace(): string | undefined { + return this.failureDetails.stackTrace; + } +} diff --git a/src/workflow/client/WorkflowState.ts b/src/workflow/client/WorkflowState.ts new file mode 100644 index 00000000..a5cbfd03 --- /dev/null +++ b/src/workflow/client/WorkflowState.ts @@ -0,0 +1,108 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { OrchestrationState } from "@microsoft/durabletask-js/orchestration/orchestration-state"; +import { WorkflowFailureDetails } from "./WorkflowFailureDetails"; +import { WorkflowRuntimeStatus, fromOrchestrationStatus } from "../runtime/WorkflowRuntimeStatus"; + +/** + * Represents the state of a workflow instance. + */ +export class WorkflowState { + private readonly _orchestrationState: OrchestrationState; + private readonly _workflowFailureDetails?: WorkflowFailureDetails; + + /** + * Creates an instance of WorkflowState. + * @param {OrchestrationState} orchestrationState - The state of the orchestration. + * @throws {Error} Throws an error if orchestrationState is null. + */ + constructor(orchestrationState: OrchestrationState) { + if (!orchestrationState) { + throw new Error("OrchestrationMetadata cannot be null"); + } + + this._orchestrationState = orchestrationState; + + const failureDetails = orchestrationState.failureDetails; + if (failureDetails) { + this._workflowFailureDetails = new WorkflowFailureDetails(failureDetails); + } + } + + /** + * Gets the name of the workflow. + * @returns {string} The name of the workflow. + */ + public get name(): string { + return this._orchestrationState.name; + } + + /** + * Gets the unique ID of the workflow instance. + * @returns {string} The unique ID of the workflow instance. + */ + public get instanceId(): string { + return this._orchestrationState.instanceId; + } + + /** + * Gets the current runtime status of the workflow instance. + * @returns {WorkflowRuntimeStatus} The current runtime status. + */ + public get runtimeStatus(): WorkflowRuntimeStatus { + return fromOrchestrationStatus(this._orchestrationState.runtimeStatus); + } + + /** + * Gets the workflow instance's creation time in UTC. + * @returns {Date} The workflow instance's creation time in UTC. + */ + public get createdAt(): Date { + return this._orchestrationState.createdAt; + } + + /** + * Gets the workflow instance's last updated time in UTC. + * @returns {Date} The workflow instance's last updated time in UTC. + */ + public get lastUpdatedAt(): Date { + return this._orchestrationState.lastUpdatedAt; + } + + /** + * Gets the workflow instance's serialized input, if any, as a string value. + * @returns {string | undefined} The workflow instance's serialized input or undefined. + */ + public get serializedInput(): string | undefined { + return this._orchestrationState.serializedInput; + } + + /** + * Gets the workflow instance's serialized output, if any, as a string value. + * @returns {string | undefined} The workflow instance's serialized output or undefined. + */ + public get serializedOutput(): string | undefined { + return this._orchestrationState.serializedOutput; + } + + /** + * Gets the failure details, if any, for the failed workflow instance. + * This method returns data only if the workflow is in the FAILED state and + * only if this instance metadata was fetched with the option to include output data. + * @returns {WorkflowFailureDetails | undefined} The failure details of the failed workflow instance or undefined. + */ + public get workflowFailureDetails(): WorkflowFailureDetails | undefined { + return this._workflowFailureDetails; + } +} diff --git a/src/workflow/internal/index.ts b/src/workflow/internal/index.ts new file mode 100644 index 00000000..a7549e54 --- /dev/null +++ b/src/workflow/internal/index.ts @@ -0,0 +1,89 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { TInput, TOutput } from "../../types/workflow/InputOutput.type"; +import { TWorkflowActivity } from "../../types/workflow/Activity.type"; +import { TWorkflow } from "../../types/workflow/Workflow.type"; +import * as grpc from "@grpc/grpc-js"; +import { WorkflowClientOptions } from "../../types/workflow/WorkflowClientOption"; +import { Settings } from "../../utils/Settings.util"; +import { GrpcEndpoint } from "../../network/GrpcEndpoint"; + +/** + * Gets the name of a function from its definition or string representation. + * + * @param fn - The function for which the name is to be retrieved. Can be either a function or a string representation of a function. + * @returns The name of the function. + * + * @typeparam TWorkflow - The type of the workflow function. + * @typeparam TInput - The type of the input for the workflow activity. + * @typeparam TOutput - The type of the output for the workflow activity. + */ +export function getFunctionName(fn: TWorkflow | TWorkflowActivity): string { + if (fn.name) { + return fn.name; + } else { + const match = fn.toString().match(/function\s*([^(]*)\(/); + if (match === null) { + throw new Error("Unable to determine function name, try to sepecify the workflow/activity name explicitly."); + } + return match[1]; + } +} + +/** + * Generates a Dapr gRPC endpoint based on the provided options or defaults. + * + * @param options - An object containing optional settings for the WorkflowClient. + * @returns A GrpcEndpoint representing the Dapr gRPC endpoint. + */ +export function generateEndpoint(options: Partial): GrpcEndpoint { + const host = options?.daprHost ?? Settings.getDefaultHost(); + const port = options?.daprPort ?? Settings.getDefaultGrpcPort(); + const uri = `${host}:${port}`; + return new GrpcEndpoint(uri); +} + +/** + * Gets the Dapr API token based on the provided options or defaults. + * + * @param options - An object containing optional settings for the WorkflowClient. + * @returns A string representing the Dapr API token, or undefined if not set. + */ +export function getDaprApiToken(options: Partial): string | undefined { + const daprApiToken = options?.daprApiToken ?? Settings.getDefaultApiToken(); + return daprApiToken; +} + +/** + * Generates a gRPC interceptor function that adds a Dapr API token to the metadata if not already present. + * This interceptor is intended for use with gRPC client calls. + * + * @param options - The gRPC call options object. + * @param nextCall - The next call function in the interceptor chain. + * @returns A gRPC InterceptingCall instance with added functionality to include a Dapr API token in the metadata. + */ +export function generateApiTokenClientInterceptors( + workflowOptions: Partial, +): (options: any, nextCall: any) => grpc.InterceptingCall { + return (options: any, nextCall: any) => { + return new grpc.InterceptingCall(nextCall(options), { + start: (metadata, listener, next) => { + if (metadata.get("dapr-api-token").length == 0) { + metadata.add("dapr-api-token", workflowOptions.daprApiToken as grpc.MetadataValue); + } + next(metadata, listener); + }, + }); + }; +} diff --git a/src/workflow/runtime/WorkflowActivityContext.ts b/src/workflow/runtime/WorkflowActivityContext.ts new file mode 100644 index 00000000..cf29962d --- /dev/null +++ b/src/workflow/runtime/WorkflowActivityContext.ts @@ -0,0 +1,46 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { ActivityContext } from "@microsoft/durabletask-js"; + +/** + * Used by activity to perform actions such as getting activity's name and + * its input. + */ +export default class WorkflowActivityContext { + private readonly _innerContext: ActivityContext; + constructor(innerContext: ActivityContext) { + if (!innerContext) { + throw new Error("ActivityContext cannot be undefined"); + } + this._innerContext = innerContext; + } + + /** + * Gets the unique identifier of the workflow instance associated with the current context. + * + * @returns {string} The unique identifier (orchestrationId) of the workflow instance. + */ + public getWorkflowInstanceId(): string { + return this._innerContext.orchestrationId; + } + + /** + * Gets the task ID (activityId) associated with the current workflow activity context. + * + * @returns {number} The task ID (activityId) of the current workflow activity. + */ + public getWorkflowActivityId(): number { + return this._innerContext.taskId; + } +} diff --git a/src/workflow/runtime/WorkflowContext.ts b/src/workflow/runtime/WorkflowContext.ts new file mode 100644 index 00000000..240a1d64 --- /dev/null +++ b/src/workflow/runtime/WorkflowContext.ts @@ -0,0 +1,157 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { OrchestrationContext } from "@microsoft/durabletask-js"; +import { Task } from "@microsoft/durabletask-js/task/task"; +import { TWorkflowActivity } from "../../types/workflow/Activity.type"; +import { TWorkflow } from "../../types/workflow/Workflow.type"; +import { getFunctionName } from "../internal"; +import { WhenAllTask } from "@microsoft/durabletask-js/task/when-all-task"; +import { whenAll, whenAny } from "@microsoft/durabletask-js/task"; +import { WhenAnyTask } from "@microsoft/durabletask-js/task/when-any-task"; +import { TInput, TOutput } from "../../types/workflow/InputOutput.type"; + +/** + * Used by workflow to perform actions such as scheduling tasks, durable timers, waiting for external events, + * and for getting basic information about the current workflow. + */ +export default class WorkflowContext { + private readonly _innerContext: OrchestrationContext; + constructor(innerContext: OrchestrationContext) { + if (!innerContext) { + throw new Error("ActivityContext cannot be undefined"); + } + this._innerContext = innerContext; + } + + /** + * Gets the unique ID of the current orchestration instance. + * @returns {string} The unique ID of the current orchestration instance + */ + public getWorkflowInstanceId(): string { + return this._innerContext.instanceId; + } + + /** + * Get the current date/time as UTC + * + * @returns {Date} The current timestamp in a way that is safe for use by orchestrator functions + */ + public getCurrentUtcDateTime(): Date { + return this._innerContext.currentUtcDateTime; + } + + /** + * Get the value indicating whether the orchestrator is replaying from history. + * + * This property is useful when there is logic that needs to run only when + * the orchestrator function is _not_ replaying. For example, certain + * types of application logging may become too noisy when duplicated as + * part of orchestrator function replay. The orchestrator code could check + * to see whether the function is being replayed and then issue the log + * statements when this value is `false`. + * + * @returns {boolean} `true` if the orchestrator function is replaying from history; otherwise, `false`. + */ + public isReplaying(): boolean { + return this._innerContext.isReplaying; + } + + /** + * Create a timer task that will fire at a specified time. + * + * @param {Date | number} fireAt The time at which the timer should fire. + * @returns {Task} A Durable Timer task that schedules the timer to wake up the orchestrator + */ + public createTimer(fireAt: Date | number): Task { + return this._innerContext.createTimer(fireAt); + } + + /** + * Schedules an activity for execution within the orchestrator. + * + * @param {TWorkflowActivity | string} activity - The activity function or its name to call. + * @param {TInput} [input] - The JSON-serializable input value for the activity function. + * @returns {Task} - A Durable Task that completes when the activity function completes. + * + * @typeparam TWorkflowActivity - The type of the activity function. + * @typeparam TInput - The type of the input for the activity. + * @typeparam TOutput - The type of the output for the activity. + */ + public callActivity(activity: TWorkflowActivity | string, input?: TInput): Task { + if (typeof activity === "string") { + return this._innerContext.callActivity(activity, input); + } + return this._innerContext.callActivity(getFunctionName(activity), input); + } + + /** + * Schedule sub-orchestrator function for execution. + * + * @param orchestrator A reference to the orchestrator function call + * @param input The JSON-serializable input value for the orchestrator function. + * @param instanceId A unique ID to use for the sub-orchestration instance. If not provided, a new GUID will be used. + * + * @returns {Task} A Durable Task that completes when the sub-orchestrator function completes. + */ + public callSubWorkflow( + orchestrator: TWorkflow | string, + input?: TInput, + instanceId?: string, + ): Task { + if (typeof orchestrator === "string") { + return this._innerContext.callSubOrchestrator(orchestrator, input, instanceId); + } + return this._innerContext.callSubOrchestrator(getFunctionName(orchestrator), input, instanceId); + } + + /** + * Wait for an event to be raised with the name "name" + * + * @param name The name of the event to wait for + * @returns {Task} A Durable Task that completes when the event is received + */ + public waitForExternalEvent(name: string): Task { + return this._innerContext.waitForExternalEvent(name); + } + + /** + * Continue the orchestration execution as a new instance + * + * @param newInput {any} The new input to use for the new orchestration instance. + * @param saveEvents {boolean} A flag indicating whether to add any unprocessed external events in the new orchestration history. + */ + public continueAsNew(newInput: any, saveEvents: boolean): void { + this._innerContext.continueAsNew(newInput, saveEvents); + } + + /** + * Returns a task that completes when all of the provided tasks complete or when one of the tasks fail + * + * @param tasks the tasks to wait for + * @returns {WhenAllTask} a task that completes when all of the provided tasks complete or when one of the tasks fail + */ + public whenAll(tasks: Task[]): WhenAllTask { + return whenAll(tasks); + } + + /** + * Returns a task that completes when any of the provided tasks complete or fail + * + * @param tasks the tasks to wait for + * @returns {WhenAnyTask} a task that completes when one of the provided tasks complete or when one of the tasks fail + */ + public whenAny(tasks: Task[]): WhenAnyTask { + return whenAny(tasks); + } +} diff --git a/src/workflow/runtime/WorkflowRuntime.ts b/src/workflow/runtime/WorkflowRuntime.ts new file mode 100644 index 00000000..bafcbd30 --- /dev/null +++ b/src/workflow/runtime/WorkflowRuntime.ts @@ -0,0 +1,129 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { ActivityContext, OrchestrationContext, TaskHubGrpcWorker } from "@microsoft/durabletask-js"; +import { TWorkflow } from "../../types/workflow/Workflow.type"; +import { TWorkflowActivity } from "../../types/workflow/Activity.type"; +import { TInput, TOutput } from "../../types/workflow/InputOutput.type"; +import WorkflowActivityContext from "./WorkflowActivityContext"; +import WorkflowContext from "./WorkflowContext"; +import { generateApiTokenClientInterceptors, generateEndpoint, getDaprApiToken } from "../internal/index"; +import { getFunctionName } from "../internal"; +import { WorkflowClientOptions } from "../../types/workflow/WorkflowClientOption"; + +/** + * Contains methods to register workflows and activities. + */ +export default class WorkflowRuntime { + private worker: TaskHubGrpcWorker; + + /** + * Initializes a new instance of the WorkflowRuntime. + * @param {WorkflowClientOptions | undefined} options - Additional options for configuring WorkflowRuntime. + */ + constructor(options: Partial = {}) { + const grpcEndpoint = generateEndpoint(options); + options.daprApiToken = getDaprApiToken(options); + this.worker = this.buildInnerWorker(grpcEndpoint.endpoint, options); + } + + private buildInnerWorker(hostAddress: string, options: Partial): TaskHubGrpcWorker { + let innerOptions = options?.grpcOptions; + if (options.daprApiToken !== undefined && options.daprApiToken !== "") { + innerOptions = { + ...innerOptions, + interceptors: [generateApiTokenClientInterceptors(options), ...(innerOptions?.interceptors ?? [])], + }; + } + return new TaskHubGrpcWorker(hostAddress, innerOptions); + } + + /** + * Registers a Workflow implementation for handling orchestrations. + * + * @param {TWorkflow} workflow - The instance of the Workflow class being registered. + */ + public registerWorkflow(workflow: TWorkflow): WorkflowRuntime { + const name = getFunctionName(workflow); + const workflowWrapper = (ctx: OrchestrationContext, input: any): any => { + const workflowContext = new WorkflowContext(ctx); + return workflow(workflowContext, input); + }; + this.worker.addNamedOrchestrator(name, workflowWrapper); + return this; + } + + /** + * Registers a Workflow implementation for handling orchestrations with a given name. + * The name provided need not be same as workflow name. + * + * @param {string} name - The name or identifier for the registered Workflow. + * @param {TWorkflow} workflow - The instance of the Workflow class being registered. + */ + public registerWorkflowWithName(name: string, workflow: TWorkflow): WorkflowRuntime { + const workflowWrapper = (ctx: OrchestrationContext, input: any): any => { + const workflowContext = new WorkflowContext(ctx); + return workflow(workflowContext, input); + }; + this.worker.addNamedOrchestrator(name, workflowWrapper); + return this; + } + + /** + * Registers an Activity object. + * + * @param {TWorkflowActivity} fn - The instance of the WorkflowActivity class being registered. + * @returns {WorkflowRuntime} The current instance of WorkflowRuntime. + */ + public registerActivity(fn: TWorkflowActivity): WorkflowRuntime { + const name = getFunctionName(fn); + const activityWrapper = (ctx: ActivityContext, intput: TInput): TOutput => { + const wfActivityContext = new WorkflowActivityContext(ctx); + return fn(wfActivityContext, intput); + }; + this.worker.addNamedActivity(name, activityWrapper); + return this; + } + + /** + * Registers an Activity object with a given name. + * The name provided need not be same as WorkflowActivity name. + * + * @param {string} name - The name or identifier for the registered Activity. + * @param {TWorkflowActivity} fn - The instance of the WorkflowActivity class being registered. + * @returns {WorkflowRuntime} The current instance of WorkflowRuntime. + */ + public registerActivityWithName(name: string, fn: TWorkflowActivity): WorkflowRuntime { + const activityWrapper = (ctx: ActivityContext, intput: TInput): any => { + const wfActivityContext = new WorkflowActivityContext(ctx); + return fn(wfActivityContext, intput); + }; + + this.worker.addNamedActivity(name, activityWrapper); + return this; + } + + /** + * Start the Workflow runtime processing items and block. + */ + public async start() { + await this.worker.start(); + } + + /** + * Stop the worker and wait for any pending work items to complete + */ + public async stop() { + await this.worker.stop(); + } +} diff --git a/src/workflow/runtime/WorkflowRuntimeStatus.ts b/src/workflow/runtime/WorkflowRuntimeStatus.ts new file mode 100644 index 00000000..df355f9c --- /dev/null +++ b/src/workflow/runtime/WorkflowRuntimeStatus.ts @@ -0,0 +1,57 @@ +/* +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { OrchestrationStatus } from "@microsoft/durabletask-js/orchestration/enum/orchestration-status.enum"; + +/** + * Enum describing the runtime status of a workflow. + */ +export enum WorkflowRuntimeStatus { + RUNNING = OrchestrationStatus.RUNNING, + COMPLETED = OrchestrationStatus.COMPLETED, + FAILED = OrchestrationStatus.FAILED, + TERMINATED = OrchestrationStatus.TERMINATED, + CONTINUED_AS_NEW = OrchestrationStatus.CONTINUED_AS_NEW, + PENDING = OrchestrationStatus.PENDING, + SUSPENDED = OrchestrationStatus.SUSPENDED, +} + +/** + * Converts an OrchestrationStatus value to the corresponding WorkflowRuntimeStatus enum value. + * + * @param {OrchestrationStatus} val - The OrchestrationStatus value to be converted. + * @returns {WorkflowRuntimeStatus} - The equivalent WorkflowRuntimeStatus enum value. + */ +export function fromOrchestrationStatus(val: OrchestrationStatus): WorkflowRuntimeStatus { + const values = Object.values(WorkflowRuntimeStatus); + const valIdx = values.findIndex((v) => v == (val as number)); + + // Return the entry of the WorkflowRuntimeStatus enum at index + const entries = Object.entries(WorkflowRuntimeStatus); + return entries[valIdx][1] as WorkflowRuntimeStatus; +} + +/** + * Converts an WorkflowRuntimeStatus value to the corresponding OrchestrationStatus enum value. + * + * @param {WorkflowRuntimeStatus} val - The WorkflowRuntimeStatus value to be converted. + * @returns {OrchestrationStatus} - The equivalent OrchestrationStatus enum value. + */ +export function toOrchestrationStatus(val: WorkflowRuntimeStatus): OrchestrationStatus { + const values = Object.values(OrchestrationStatus); + const valIdx = values.findIndex((v) => v == (val as number)); + + // Return the entry of the WorkflowRuntimeStatus enum at index + const entries = Object.entries(OrchestrationStatus); + return entries[valIdx][1] as OrchestrationStatus; +} diff --git a/test/components/workflow/redis.yaml b/test/components/workflow/redis.yaml new file mode 100644 index 00000000..175c0e7a --- /dev/null +++ b/test/components/workflow/redis.yaml @@ -0,0 +1,33 @@ +# +# Copyright 2022 The Dapr Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# https://docs.dapr.io/reference/components-reference/supported-bindings/rabbitmq/ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: state-redis + namespace: default +spec: + type: state.redis + version: v1 + metadata: + - name: redisHost + value: localhost:6379 + - name: redisPassword + value: "" + - name: enableTLS + value: "false" + - name: failover + value: "false" + - name: actorStateStore + value: "true" \ No newline at end of file diff --git a/test/e2e/workflow/workflow.test.ts b/test/e2e/workflow/workflow.test.ts new file mode 100644 index 00000000..373a0cbe --- /dev/null +++ b/test/e2e/workflow/workflow.test.ts @@ -0,0 +1,374 @@ +/* +Copyright 2022 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import DaprWorkflowClient from "../../../src/workflow/client/DaprWorkflowClient"; +import WorkflowContext from "../../../src/workflow/runtime/WorkflowContext"; +import WorkflowRuntime from "../../../src/workflow/runtime/WorkflowRuntime"; +import { TWorkflow } from "../../../src/types/workflow/Workflow.type"; +import { getFunctionName } from "../../../src/workflow/internal"; +import { WorkflowRuntimeStatus } from "../../../src/workflow/runtime/WorkflowRuntimeStatus"; +import WorkflowActivityContext from "../../../src/workflow/runtime/WorkflowActivityContext"; +import { Task } from "@microsoft/durabletask-js/task/task"; + +const clientHost = "localhost"; +const clientPort = "4001"; + +describe("Workflow", () => { + let workflowClient: DaprWorkflowClient; + let workflowRuntime: WorkflowRuntime; + + beforeEach(async () => { + // Start a worker, which will connect to the sidecar in a background thread + workflowClient = new DaprWorkflowClient({ + daprHost: clientHost, + daprPort: clientPort, + }); + workflowRuntime = new WorkflowRuntime({ + daprHost: clientHost, + daprPort: clientPort, + }); + }); + + afterEach(async () => { + await workflowRuntime.stop(); + await workflowClient.stop(); + }); + + it("should be able to run an empty orchestration", async () => { + let invoked = false; + const emptyWorkflow: TWorkflow = async (_: WorkflowContext, __: any) => { + invoked = true; + }; + workflowRuntime.registerWorkflow(emptyWorkflow); + await workflowRuntime.start(); + + const id = await workflowClient.scheduleNewWorkflow(emptyWorkflow); + const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30); + + expect(invoked).toBe(true); + expect(state).toBeDefined(); + expect(state?.name).toEqual(getFunctionName(emptyWorkflow)); + expect(state?.instanceId).toEqual(id); + expect(state?.workflowFailureDetails).toBeUndefined(); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.COMPLETED); + }, 31000); + + it("should be able to run an activity sequence", async () => { + const plusOne = async (_: WorkflowActivityContext, input: number) => { + return input + 1; + }; + + const sequenceWorkflow: TWorkflow = async function* (ctx: WorkflowContext, startVal: number): any { + const numbers = [startVal]; + let current = startVal; + + for (let i = 0; i < 10; i++) { + current = yield ctx.callActivity(plusOne, current); + numbers.push(current); + } + + return numbers; + }; + + workflowRuntime.registerWorkflow(sequenceWorkflow).registerActivity(plusOne); + await workflowRuntime.start(); + + const id = await workflowClient.scheduleNewWorkflow(sequenceWorkflow, 1); + const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.name).toEqual(getFunctionName(sequenceWorkflow)); + expect(state?.instanceId).toEqual(id); + expect(state?.workflowFailureDetails).toBeUndefined(); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.COMPLETED); + expect(state?.serializedInput).toEqual(JSON.stringify(1)); + expect(state?.serializedOutput).toEqual(JSON.stringify([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])); + }, 31000); + + it("should be able to run fan-out/fan-in", async () => { + let activityCounter = 0; + + const incrementActivity = (_: WorkflowActivityContext) => { + activityCounter++; + }; + + const sequenceWorkflow: TWorkflow = async function* (ctx: WorkflowContext, count: number): any { + // Fan out to multiple sub-orchestrations + const tasks: Task[] = []; + + for (let i = 0; i < count; i++) { + tasks.push(ctx.callActivity(incrementActivity)); + } + + // Wait for all the sub-orchestrations to complete + yield ctx.whenAll(tasks); + }; + + workflowRuntime.registerWorkflow(sequenceWorkflow).registerActivity(incrementActivity); + await workflowRuntime.start(); + + const id = await workflowClient.scheduleNewWorkflow(sequenceWorkflow, 10); + const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 10); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.COMPLETED); + expect(state?.workflowFailureDetails).toBeUndefined(); + expect(activityCounter).toEqual(10); + }, 31000); + + it("should be able to use the sub-orchestration", async () => { + let activityCounter = 0; + + const incrementActivity = (_: WorkflowActivityContext) => { + activityCounter++; + }; + + const childWorkflow: TWorkflow = async function* (ctx: WorkflowContext): any { + yield ctx.callActivity(incrementActivity); + }; + + const parentWorkflow: TWorkflow = async function* (ctx: WorkflowContext): any { + // Call sub-orchestration + yield ctx.callSubWorkflow(childWorkflow); + }; + + workflowRuntime + .registerActivity(incrementActivity) + .registerWorkflow(childWorkflow) + .registerWorkflow(parentWorkflow); + await workflowRuntime.start(); + + const id = await workflowClient.scheduleNewWorkflow(parentWorkflow, 10); + const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.COMPLETED); + expect(state?.workflowFailureDetails).toBeUndefined(); + expect(activityCounter).toEqual(1); + }, 31000); + + it("should allow waiting for multiple external events", async () => { + const workflow: TWorkflow = async function* (ctx: WorkflowContext, _: any): any { + const a = yield ctx.waitForExternalEvent("A"); + const b = yield ctx.waitForExternalEvent("B"); + const c = yield ctx.waitForExternalEvent("C"); + return [a, b, c]; + }; + + workflowRuntime.registerWorkflow(workflow); + await workflowRuntime.start(); + + // Send events to the client immediately + const id = await workflowClient.scheduleNewWorkflow(workflow); + workflowClient.raiseEvent(id, "A", "a"); + workflowClient.raiseEvent(id, "B", "b"); + workflowClient.raiseEvent(id, "C", "c"); + const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.COMPLETED); + expect(state?.serializedOutput).toEqual(JSON.stringify(["a", "b", "c"])); + }, 31000); + + it("should be able to run an single timer", async () => { + const delay = 3; + const singleTimerWorkflow: TWorkflow = async function* (ctx: WorkflowContext, _: number): any { + // seems there is a issue from durabletask-sidecar. + // TODO: Once transfer to durabletask-go, reset the timer + yield ctx.createTimer(delay + 1); + }; + + workflowRuntime.registerWorkflow(singleTimerWorkflow); + await workflowRuntime.start(); + + const id = await workflowClient.scheduleNewWorkflow(singleTimerWorkflow); + const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30); + + let expectedCompletionSecond = state?.createdAt?.getTime() ?? 0; + if (state && state.createdAt !== undefined) { + expectedCompletionSecond += delay * 1000; + } + expect(expectedCompletionSecond).toBeDefined(); + const actualCompletionSecond = state?.lastUpdatedAt?.getTime() ?? 0; + expect(actualCompletionSecond).toBeDefined(); + + expect(state).toBeDefined(); + expect(state?.name).toEqual(getFunctionName(singleTimerWorkflow)); + expect(state?.instanceId).toEqual(id); + expect(state?.workflowFailureDetails).toBeUndefined(); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.COMPLETED); + expect(state?.createdAt).toBeDefined(); + expect(state?.lastUpdatedAt).toBeDefined(); + expect(expectedCompletionSecond).toBeLessThanOrEqual(actualCompletionSecond); + }, 31000); + + it("should wait for external events with a timeout - true", async () => { + const shouldRaiseEvent = true; + const workflow: TWorkflow = async function* (ctx: WorkflowContext, _: any): any { + const approval = ctx.waitForExternalEvent("Approval"); + const timeout = ctx.createTimer(3); + const winner = yield ctx.whenAny([approval, timeout]); + + if (winner == approval) { + return "approved"; + } else { + return "timed out"; + } + }; + + workflowRuntime.registerWorkflow(workflow); + await workflowRuntime.start(); + + // Send events to the client immediately + const id = await workflowClient.scheduleNewWorkflow(workflow); + + if (shouldRaiseEvent) { + workflowClient.raiseEvent(id, "Approval"); + } + + const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30); + + expect(state); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.COMPLETED); + + if (shouldRaiseEvent) { + expect(state?.serializedOutput).toEqual(JSON.stringify("approved")); + } else { + expect(state?.serializedOutput).toEqual(JSON.stringify("timed out")); + } + }, 31000); + + it("should wait for external events with a timeout - false", async () => { + const shouldRaiseEvent = false; + const workflow: TWorkflow = async function* (ctx: WorkflowContext, _: any): any { + const approval = ctx.waitForExternalEvent("Approval"); + const timeout = ctx.createTimer(3); + const winner = yield ctx.whenAny([approval, timeout]); + + if (winner == approval) { + return "approved"; + } else { + return "timed out"; + } + }; + + workflowRuntime.registerWorkflow(workflow); + await workflowRuntime.start(); + + // Send events to the client immediately + const id = await workflowClient.scheduleNewWorkflow(workflow); + + if (shouldRaiseEvent) { + workflowClient.raiseEvent(id, "Approval"); + } + + const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.COMPLETED); + + if (shouldRaiseEvent) { + expect(state?.serializedOutput).toEqual(JSON.stringify("approved")); + } else { + expect(state?.serializedOutput).toEqual(JSON.stringify("timed out")); + } + }, 31000); + + it("should be able to terminate an orchestration", async () => { + const workflow: TWorkflow = async function* (ctx: WorkflowContext, _: any): any { + const res = yield ctx.waitForExternalEvent("my_event"); + return res; + }; + + workflowRuntime.registerWorkflow(workflow); + await workflowRuntime.start(); + + const id = await workflowClient.scheduleNewWorkflow(workflow); + let state = await workflowClient.waitForWorkflowStart(id, undefined, 30); + expect(state); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.RUNNING); + + await workflowClient.terminateWorkflow(id, "some reason for termination"); + state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30); + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.TERMINATED); + expect(state?.serializedOutput).toEqual(JSON.stringify("some reason for termination")); + }, 31000); + + it("should allow to continue as new", async () => { + const workflow: TWorkflow = async (ctx: WorkflowContext, input: number) => { + if (input < 10) { + ctx.continueAsNew(input + 1, true); + } else { + return input; + } + }; + + workflowRuntime.registerWorkflow(workflow); + await workflowRuntime.start(); + + const id = await workflowClient.scheduleNewWorkflow(workflow, 1); + + const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30); + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.COMPLETED); + expect(state?.serializedOutput).toEqual(JSON.stringify(10)); + }, 31000); + + it("should be able to run an single orchestration without activity", async () => { + const workflow: TWorkflow = async (_: WorkflowContext, startVal: number) => { + return startVal + 1; + }; + + workflowRuntime.registerWorkflow(workflow); + await workflowRuntime.start(); + + const id = await workflowClient.scheduleNewWorkflow(workflow, 15); + const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.name).toEqual(getFunctionName(workflow)); + expect(state?.instanceId).toEqual(id); + expect(state?.workflowFailureDetails).toBeUndefined(); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.COMPLETED); + expect(state?.serializedInput).toEqual(JSON.stringify(15)); + expect(state?.serializedOutput).toEqual(JSON.stringify(16)); + }, 31000); + + it("should be able to purge orchestration by id", async () => { + const plusOneActivity = async (_: WorkflowActivityContext, input: number) => { + return input + 1; + }; + + const workflow: TWorkflow = async function* (ctx: WorkflowContext, startVal: number): any { + return yield ctx.callActivity(plusOneActivity, startVal); + }; + + workflowRuntime.registerWorkflow(workflow).registerActivity(plusOneActivity); + await workflowRuntime.start(); + + const id = await workflowClient.scheduleNewWorkflow(workflow, 1); + const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.name).toEqual(getFunctionName(workflow)); + expect(state?.instanceId).toEqual(id); + expect(state?.workflowFailureDetails).toBeUndefined(); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.COMPLETED); + expect(state?.serializedInput).toEqual(JSON.stringify(1)); + expect(state?.serializedOutput).toEqual(JSON.stringify(2)); + + const purgeResult = await workflowClient.purgeWorkflow(id); + expect(purgeResult).toEqual(true); + }, 31000); +}); diff --git a/test/unit/workflow/workflowRuntimeStatus.test.ts b/test/unit/workflow/workflowRuntimeStatus.test.ts new file mode 100644 index 00000000..c813852b --- /dev/null +++ b/test/unit/workflow/workflowRuntimeStatus.test.ts @@ -0,0 +1,114 @@ +/* +Copyright 2022 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { OrchestrationStatus } from "@microsoft/durabletask-js/orchestration/enum/orchestration-status.enum"; +import { + WorkflowRuntimeStatus, + fromOrchestrationStatus, + toOrchestrationStatus, +} from "../../../src/workflow/runtime/WorkflowRuntimeStatus"; +import { getFunctionName } from "../../../src/workflow/internal"; +import { TWorkflow } from "../../../src/types/workflow/Workflow.type"; +import WorkflowContext from "../../../src/workflow/runtime/WorkflowContext"; +import WorkflowActivityContext from "../../../src/workflow/runtime/WorkflowActivityContext"; + +describe("Workflow Runtime Status", () => { + describe("convert runtime status", () => { + const testCases = [ + { + orchestrationStatus: OrchestrationStatus.RUNNING, + workflowRuntimeStatus: WorkflowRuntimeStatus.RUNNING, + }, + + { + orchestrationStatus: OrchestrationStatus.COMPLETED, + workflowRuntimeStatus: WorkflowRuntimeStatus.COMPLETED, + }, + + { + orchestrationStatus: OrchestrationStatus.FAILED, + workflowRuntimeStatus: WorkflowRuntimeStatus.FAILED, + }, + + { + orchestrationStatus: OrchestrationStatus.TERMINATED, + workflowRuntimeStatus: WorkflowRuntimeStatus.TERMINATED, + }, + + { + orchestrationStatus: OrchestrationStatus.CONTINUED_AS_NEW, + workflowRuntimeStatus: WorkflowRuntimeStatus.CONTINUED_AS_NEW, + }, + + { + orchestrationStatus: OrchestrationStatus.PENDING, + workflowRuntimeStatus: WorkflowRuntimeStatus.PENDING, + }, + + { + orchestrationStatus: OrchestrationStatus.SUSPENDED, + workflowRuntimeStatus: WorkflowRuntimeStatus.SUSPENDED, + }, + ]; + + testCases.forEach((testCase) => { + test("Should be able to convert between orchestration status to workflow runtime status", () => { + expect(fromOrchestrationStatus(testCase.orchestrationStatus)).toEqual(testCase.workflowRuntimeStatus); + expect(toOrchestrationStatus(testCase.workflowRuntimeStatus)).toEqual(testCase.orchestrationStatus); + }); + }); + }); + + describe("getFunctionName", () => { + it("should return the name of the function", () => { + const namedFunction = function exampleFunction(): number { + return 1; + }; + + const result = getFunctionName(namedFunction); + + expect(result).toBe("exampleFunction"); + }); + + it("should extract the name from the string representation of the function", () => { + const anonymousFunction = function (): number { + return 1; + }; + + const result = getFunctionName(anonymousFunction); + + expect(result).not.toBeUndefined(); + expect(typeof result).toBe("string"); + }); + + it("should handle TWorkflow type", () => { + const emptyWorkflow: TWorkflow = async (_: WorkflowContext, __: any) => { + return 1; + }; + + const result = getFunctionName(emptyWorkflow); + + expect(result).toBe("emptyWorkflow"); + }); + + it("should handle TWorkflow type", () => { + const emptyWorkflowActivity = async (_: WorkflowActivityContext) => { + return 1; + }; + + const result = getFunctionName(emptyWorkflowActivity); + + expect(result).toBe("emptyWorkflowActivity"); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 4c9ef303..f86af11a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ "resolveJsonModule": true, - "target": "es2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, "lib": [] /* Specify library files to be included in the compilation. */, // "allowJs": true, /* Allow javascript files to be compiled. */