diff --git a/.gitmodules b/.gitmodules index 6010b33a394..113d4ec4682 100644 --- a/.gitmodules +++ b/.gitmodules @@ -8,9 +8,6 @@ [submodule "sdkdocs/go"] path = sdkdocs/go url = https://github.com/dapr/go-sdk.git -[submodule "sdkdocs/js"] - path = sdkdocs/js - url = https://github.com/dapr/js-sdk.git [submodule "sdkdocs/pluggable-components/go"] path = sdkdocs/pluggable-components/go url = https://github.com/dapr-sandbox/components-go-sdk diff --git a/hugo.yaml b/hugo.yaml index d7f1971b618..86433dcc1c9 100644 --- a/hugo.yaml +++ b/hugo.yaml @@ -302,10 +302,10 @@ module: - source: sdkdocs/java/content/en/java-sdk-contributing target: content/contributing/sdk-contrib/ lang: en - - source: sdkdocs/js/daprdocs/content/en/js-sdk-docs + - source: sdkdocs/js/content/en/js-sdk-docs target: content/developing-applications/sdks/js lang: en - - source: sdkdocs/js/daprdocs/content/en/js-sdk-contributing + - source: sdkdocs/js/content/en/js-sdk-contributing target: content/contributing/sdk-contrib/ lang: en - source: sdkdocs/rust/daprdocs/content/en/rust-sdk-docs diff --git a/sdkdocs/js b/sdkdocs/js deleted file mode 160000 index 26e8be8931a..00000000000 --- a/sdkdocs/js +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 26e8be8931aed2404e0e382b6c61264d1b64f0de diff --git a/sdkdocs/js/README.md b/sdkdocs/js/README.md new file mode 100644 index 00000000000..74f25a7b3c6 --- /dev/null +++ b/sdkdocs/js/README.md @@ -0,0 +1,25 @@ +# Dapr JavaScript SDK documentation + +This page covers how the documentation is structured for the Dapr JavaScript SDK + +## Dapr Docs + +All Dapr documentation is hosted at [docs.dapr.io](https://docs.dapr.io), including the docs for the [JavaScript SDK](https://docs.dapr.io/developing-applications/sdks/javascript/). Head over there if you want to read the docs. + +### JavaScript SDK docs source + +Although the docs site code and content is in the [docs repo](https://github.com/dapr/docs), the JavaScript SDK content and images are within the `content` and `static` directories, respectively. + +This allows separation of roles and expertise between maintainers, and makes it easy to find the docs files you are looking for. + +## Writing JavaScript SDK docs + +To get up and running to write JavaScript SDK docs, visit the [docs repo](https://github.com/dapr/docs) to initialize your environment. It will clone both the docs repo and this repo, so you can make changes and see it rendered within the site instantly, as well as commit and PR into this repo. + +Make sure to read the [docs contributing guide](https://docs.dapr.io/contributing/contributing-docs/) for information on style/semantics/etc. + +## Docs architecture + +The docs site is built on [Hugo](https://gohugo.io), which lives in the docs repo. This repo is setup as a git submodule so that when the repo is cloned and initialized, the javascript-sdk repo, along with the docs, are cloned as well. + +Then, in the Hugo configuration file, the `daprdocs/content` and `daprdocs/static` directories are redirected to the `daprdocs/developing-applications/sdks/javascript` and `static/javascript` directories, respectively. Thus, all the content within this repo is folded into the main docs site. diff --git a/sdkdocs/js/content/en/js-sdk-contributing/js-contributing.md b/sdkdocs/js/content/en/js-sdk-contributing/js-contributing.md new file mode 100644 index 00000000000..e7ad14e510c --- /dev/null +++ b/sdkdocs/js/content/en/js-sdk-contributing/js-contributing.md @@ -0,0 +1,130 @@ +--- +type: docs +title: "Contributing to the JavaScript SDK" +linkTitle: "JavaScript SDK" +weight: 3000 +description: Guidelines for contributing to the Dapr JavaScript SDK +--- + +When contributing to the [JavaScript SDK](https://github.com/dapr/js-sdk) the following rules and best-practices should be followed. + +💡 You can run `npm pretty-fix` to run prettier on all your files + +## Commit Guidelines + +The Dapr Javascript SDK uses the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) +specification. The automatic changelog tool uses these to automatically generate +a changelog based on the commit messages. Here's a guide to writing a commit message +to allow this: + +### Format + +``` +type(scope)!: subject +``` + +- `type`: the type of the commit is one of the following: + + - `feat`: new features. + - `fix`: bug fixes. + - `docs`: documentation changes. + - `refactor`: refactor of a particular code section without introducing + new features or bug fixes. + - `style`: code style improvements. + - `perf`: performance improvements. + - `test`: changes to the test suite. + - `ci`: changes to the CI system. + - `build`: changes to the build system (we don't yet have one so this shouldn't apply). + - `chore`: for other changes that don't match previous types. This doesn't appear + in the changelog. + +- `scope`: section of the codebase that the commit makes changes to. If it makes changes to + many sections, or if no section in particular is modified, leave blank without the parentheses. + Examples: + + - Commit that adds a `test`: + + ``` + test(actors): add an actor test + ``` + + - Commit that changes many things at once: + + ``` + style: adopt eslint + ``` + + For changes to examples, the scope should be the example name with the `examples/` prefix: + + - ❌ `fix(agnoster): commit subject` + - ✅ `fix(examples/http/actor): commit subject` + +- `!`: this goes after the `scope` (or the `type` if scope is empty), to indicate that the commit + introduces breaking changes. + + Optionally, you can specify a message that the changelog tool will display to the user to indicate + what's changed and what they can do to deal with it. You can use multiple lines to type this message; + the changelog parser will keep reading until the end of the commit message or until it finds an empty + line. + + Example (made up): + + ``` + style(agnoster)!: change dirty git repo glyph + + BREAKING CHANGE: the glyph to indicate when a git repository is dirty has + changed from a Powerline character to a standard UTF-8 emoji. + + Fixes #420 + + Co-authored-by: Username + ``` + +- `subject`: a brief description of the changes. This will be displayed in the changelog. If you need + to specify other details you can use the commit body but it won't be visible. + + Formatting tricks: the commit subject may contain: + + - Links to related issues or PRs by writing `#issue`. This will be highlighted by the changelog tool: + + ``` + feat(archlinux): add support for aura AUR helper (#9467) + ``` + + - Formatted inline code by using backticks: the text inbetween backticks will also be highlighted by + the changelog tool: + ``` + feat(shell-proxy): enable unexported `DEFAULT_PROXY` setting (#9774) + ``` + +### Style + +Try to keep the first commit line short. This is harder to do using this commit style but try to be +concise and if you need more space, you can use the commit body. Try to make sure that the commit +subject is clear and precise enough that users will know what change by just looking at the changelog. + +## Github Dapr Bot Commands + +Checkout the [daprbot documentation](https://docs.dapr.io/contributing/daprbot/) for Github commands you can run in this repo for common tasks. For example, you can run the `/assign` (as a comment on an issue) to assign issues to a user or group of users. + +## Coding Rules + +To ensure consistency throughout the source code, keep these rules in mind as you are working: + +- All features or bug fixes **must be tested** by one or more specs (unit-tests). +- All public API methods **must be documented**. +- We follow [ESLint RecommendedRules](https://eslint.org/docs/rules/). + +## Examples + +The `examples` directory contains code samples for users to run to try out specific functionality of the various JavaScript SDK packages and extensions. When writing new and updated samples keep in mind: + +- All examples should be runnable on Windows, Linux, and MacOS. While JavaScript code is consistent among operating systems, any pre/post example commands should provide options through [tabpane]({{% ref "contributing-docs.md#tabbed-content" %}}). +- Contain steps to download/install any required pre-requisites. Someone coming in with a fresh OS install should be able to start on the example and complete it without an error. Links to external download pages are fine. + +## Docs + +The `daprdocs` directory contains the markdown files that are rendered into the [Dapr Docs](https://docs.dapr.io) website. When the documentation website is built, this repo is cloned and configured so that its contents are rendered with the docs content. When writing docs, keep in mind: + +- All rules in the [docs guide]({{% ref contributing-docs.md %}}) should be followed in addition to these. +- All files and directories should be prefixed with `js-` to ensure all file/directory names are globally unique across all Dapr documentation. diff --git a/sdkdocs/js/content/en/js-sdk-docs/_index.md b/sdkdocs/js/content/en/js-sdk-docs/_index.md new file mode 100644 index 00000000000..565779d48c5 --- /dev/null +++ b/sdkdocs/js/content/en/js-sdk-docs/_index.md @@ -0,0 +1,84 @@ +--- +type: docs +title: "JavaScript SDK" +linkTitle: "JavaScript" +weight: 1000 +description: JavaScript SDK packages for developing Dapr applications +no_list: true +cascade: + github_repo: https://github.com/dapr/js-sdk + github_subdir: daprdocs/content/en/js-sdk-docs + path_base_for_github_subdir: content/en/developing-applications/sdks/js/ + github_branch: main +--- + +A client library for building Dapr apps in JavaScript and TypeScript. This client abstracts the public Dapr APIs like service to service invocation, state management, pub/sub, secrets, and much more, and provides a simple, intuitive API for building applications. + +## Installation + +To get started with the JavaScript SDK, install the Dapr JavaScript SDK package from [NPM](https://www.npmjs.com/package/@dapr/dapr): + +```bash +npm install --save @dapr/dapr +``` + +## Structure + +The Dapr JavaScript SDK contains two major components: + +- **DaprServer**: to manage all Dapr sidecar to application communication. +- **DaprClient**: to manage all application to Dapr sidecar communication. + +The above communication can be configured to use either of the gRPC or HTTP protocols. + + + + + + +
Dapr Server Dapr Client
+ +## Getting Started + +To help you get started, check out the resources below: + +
+
+
+
Client
+

Create a JavaScript client and interact with the Dapr sidecar and other Dapr applications (e.g., publishing events, output binding support, etc.).

+ +
+
+
+
+
Server
+

Create a JavaScript server and let the Dapr sidecar interact with your application (e.g., subscribing to events, input binding support, etc.).

+ +
+
+
+
+
Actors
+

Create virtual actors with state, reminders/timers, and methods.

+ +
+
+
+
+
+
+
+
Logging
+

Configure and customize the SDK logging.

+ +
+
+
+
+
Examples
+

Clone the JavaScript SDK source code and try out some of the examples to get started quickly.

+ +
+
+
diff --git a/sdkdocs/js/content/en/js-sdk-docs/images/dapr-client.jpg b/sdkdocs/js/content/en/js-sdk-docs/images/dapr-client.jpg new file mode 100644 index 00000000000..f86e9960bca Binary files /dev/null and b/sdkdocs/js/content/en/js-sdk-docs/images/dapr-client.jpg differ diff --git a/sdkdocs/js/content/en/js-sdk-docs/images/dapr-server.jpg b/sdkdocs/js/content/en/js-sdk-docs/images/dapr-server.jpg new file mode 100644 index 00000000000..26a239680c4 Binary files /dev/null and b/sdkdocs/js/content/en/js-sdk-docs/images/dapr-server.jpg differ diff --git a/sdkdocs/js/content/en/js-sdk-docs/js-actors/_index.md b/sdkdocs/js/content/en/js-sdk-docs/js-actors/_index.md new file mode 100644 index 00000000000..f9d79b1058b --- /dev/null +++ b/sdkdocs/js/content/en/js-sdk-docs/js-actors/_index.md @@ -0,0 +1,258 @@ +--- +type: docs +title: "JavaScript SDK for Actors" +linkTitle: "Actors" +weight: 3000 +description: How to get up and running with Actors using the Dapr JavaScript SDK +--- + +The Dapr actors package allows you to interact with Dapr virtual actors from a JavaScript application. The examples below demonstrate how to use the JavaScript SDK for interacting with virtual actors. + +For a more in-depth overview of Dapr actors, visit the [actors overview page]({{% ref actors-overview %}}). + +## Pre-requisites + +- [Dapr CLI]({{% ref install-dapr-cli.md %}}) installed +- Initialized [Dapr environment]({{% ref install-dapr-selfhost.md %}}) +- [Latest LTS version of Node or greater](https://nodejs.org/en/) +- [JavaScript NPM package installed](https://www.npmjs.com/package/@dapr/dapr) + +## Scenario + +The below code examples loosely describe the scenario of a Parking Garage Spot Monitoring System, which can be seen in this [video](https://www.youtube.com/watch?v=eJCu6a-x9uo&t=3785) by Mark Russinovich. + +A parking garage consists of hundreds of parking spaces, where each parking space includes a sensor that provides updates to a centralized monitoring system. The parking space sensors (our actors) detect if a parking space is occupied or available. + +To jump in and run this example yourself, clone the source code, which can be found in the [JavaScript SDK examples directory](https://github.com/dapr/js-sdk/tree/main/examples/http/actor-parking-sensor). + +## Actor Interface + +The actor interface defines the contract that is shared between the actor implementation and the clients calling the actor. In the example below, we have created an interace for a parking garage sensor. Each sensor has 2 methods: `carEnter` and `carLeave`, which defines the state of the parking space: + +```ts +export default interface ParkingSensorInterface { + carEnter(): Promise; + carLeave(): Promise; +} +``` + +## Actor Implementation + +An actor implementation defines a class by extending the base type `AbstractActor` and implementing the actor interface (`ParkingSensorInterface` in this case). + +The following code describes an actor implementation along with a few helper methods. + +```ts +import { AbstractActor } from "@dapr/dapr"; +import ParkingSensorInterface from "./ParkingSensorInterface"; + +export default class ParkingSensorImpl extends AbstractActor implements ParkingSensorInterface { + async carEnter(): Promise { + // Implementation that updates state that this parking spaces is occupied. + } + + async carLeave(): Promise { + // Implementation that updates state that this parking spaces is available. + } + + private async getInfo(): Promise { + // Implementation of requesting an update from the parking space sensor. + } + + /** + * @override + */ + async onActivate(): Promise { + // Initialization logic called by AbstractActor. + } +} +``` + +### Configuring Actor Runtime + +To configure actor runtime, use the `DaprClientOptions`. The various parameters and their default values are documented at [How-to: Use virtual actors in Dapr](https://docs.dapr.io/developing-applications/building-blocks/actors/howto-actors/#configuration-parameters). + +Note, the timeouts and intervals should be formatted as [time.ParseDuration](https://pkg.go.dev/time#ParseDuration) strings. + +```typescript +import { CommunicationProtocolEnum, DaprClient, DaprServer } from "@dapr/dapr"; + +// Configure the actor runtime with the DaprClientOptions. +const clientOptions = { + daprHost: daprHost, + daprPort: daprPort, + communicationProtocol: CommunicationProtocolEnum.HTTP, + actor: { + actorIdleTimeout: "1h", + actorScanInterval: "30s", + drainOngoingCallTimeout: "1m", + drainRebalancedActors: true, + reentrancy: { + enabled: true, + maxStackDepth: 32, + }, + remindersStoragePartitions: 0, + }, +}; + +// Use the options when creating DaprServer and DaprClient. + +// Note, DaprServer creates a DaprClient internally, which needs to be configured with clientOptions. +const server = new DaprServer({ serverHost, serverPort, clientOptions }); + +const client = new DaprClient(clientOptions); +``` + +## Registering Actors + +Initialize and register your actors by using the `DaprServer` package: + +```typescript +import { DaprServer } from "@dapr/dapr"; +import ParkingSensorImpl from "./ParkingSensorImpl"; + +const daprHost = "127.0.0.1"; +const daprPort = "50000"; +const serverHost = "127.0.0.1"; +const serverPort = "50001"; + +const server = new DaprServer({ + serverHost, + serverPort, + clientOptions: { + daprHost, + daprPort, + }, +}); + +await server.actor.init(); // Let the server know we need actors +server.actor.registerActor(ParkingSensorImpl); // Register the actor +await server.start(); // Start the server + +// To get the registered actors, you can invoke `getRegisteredActors`: +const resRegisteredActors = await server.actor.getRegisteredActors(); +console.log(`Registered Actors: ${JSON.stringify(resRegisteredActors)}`); +``` + +## Invoking Actor Methods + +After Actors are registered, create a Proxy object that implements `ParkingSensorInterface` using the `ActorProxyBuilder`. You can invoke the actor methods by directly calling methods on the Proxy object. Internally, it translates to making a network call to the Actor API and fetches the result back. + +```typescript +import { ActorId, DaprClient } from "@dapr/dapr"; +import ParkingSensorImpl from "./ParkingSensorImpl"; +import ParkingSensorInterface from "./ParkingSensorInterface"; + +const daprHost = "127.0.0.1"; +const daprPort = "50000"; + +const client = new DaprClient({ daprHost, daprPort }); + +// Create a new actor builder. It can be used to create multiple actors of a type. +const builder = new ActorProxyBuilder(ParkingSensorImpl, client); + +// Create a new actor instance. +const actor = builder.build(new ActorId("my-actor")); +// Or alternatively, use a random ID +// const actor = builder.build(ActorId.createRandomId()); + +// Invoke the method. +await actor.carEnter(); +``` + +## Using states with Actor + +```ts +import { AbstractActor } from "@dapr/dapr"; +import ActorStateInterface from "./ActorStateInterface"; + +export default class ActorStateExample extends AbstractActor implements ActorStateInterface { + async setState(key: string, value: any): Promise { + await this.getStateManager().setState(key, value); + await this.getStateManager().saveState(); + } + + async removeState(key: string): Promise { + await this.getStateManager().removeState(key); + await this.getStateManager().saveState(); + } + + // getState with a specific type + async getState(key: string): Promise { + return await this.getStateManager().getState(key); + } + + // getState without type as `any` + async getState(key: string): Promise { + return await this.getStateManager().getState(key); + } +} +``` + +## Actor Timers and Reminders + +The JS SDK supports actors that can schedule periodic work on themselves by registering either timers or reminders. The main difference between timers and reminders is that the Dapr actor runtime does not retain any information about timers after deactivation, but persists reminders information using the Dapr actor state provider. + +This distinction allows users to trade off between light-weight but stateless timers versus more resource-demanding but stateful reminders. + +The scheduling interface of timers and reminders is identical. For an more in-depth look at the scheduling configurations see the [actors timers and reminders docs]({{% ref "howto-actors.md#actor-timers-and-reminders" %}}). + +### Actor Timers + +```typescript +// ... + +const actor = builder.build(new ActorId("my-actor")); + +// Register a timer +await actor.registerActorTimer( + "timer-id", // Unique name of the timer. + "cb-method", // Callback method to execute when timer is fired. + Temporal.Duration.from({ seconds: 2 }), // DueTime + Temporal.Duration.from({ seconds: 1 }), // Period + Temporal.Duration.from({ seconds: 1 }), // TTL + 50, // State to be sent to timer callback. +); + +// Delete the timer +await actor.unregisterActorTimer("timer-id"); +``` + +### Actor Reminders + +```typescript +// ... + +const actor = builder.build(new ActorId("my-actor")); + +// Register a reminder, it has a default callback: `receiveReminder` +await actor.registerActorReminder( + "reminder-id", // Unique name of the reminder. + Temporal.Duration.from({ seconds: 2 }), // DueTime + Temporal.Duration.from({ seconds: 1 }), // Period + Temporal.Duration.from({ seconds: 1 }), // TTL + 100, // State to be sent to reminder callback. +); + +// Delete the reminder +await actor.unregisterActorReminder("reminder-id"); +``` + +To handle the callback, you need to override the default `receiveReminder` implementation in your actor. For example, from our original actor implementation: + +```ts +export default class ParkingSensorImpl extends AbstractActor implements ParkingSensorInterface { + // ... + + /** + * @override + */ + async receiveReminder(state: any): Promise { + // handle stuff here + } + + // ... +} +``` + +For a full guide on actors, visit [How-To: Use virtual actors in Dapr]({{% ref howto-actors.md %}}). diff --git a/sdkdocs/js/content/en/js-sdk-docs/js-client/_index.md b/sdkdocs/js/content/en/js-sdk-docs/js-client/_index.md new file mode 100644 index 00000000000..51b17299115 --- /dev/null +++ b/sdkdocs/js/content/en/js-sdk-docs/js-client/_index.md @@ -0,0 +1,728 @@ +--- +type: docs +title: "JavaScript Client SDK" +linkTitle: "Client" +weight: 1000 +description: JavaScript Client SDK for developing Dapr applications +--- + +## Introduction + +The Dapr Client allows you to communicate with the Dapr Sidecar and get access to its client facing features such as Publishing Events, Invoking Output Bindings, State Management, Secret Management, and much more. + +## Pre-requisites + +- [Dapr CLI]({{% ref install-dapr-cli.md %}}) installed +- Initialized [Dapr environment]({{% ref install-dapr-selfhost.md %}}) +- [Latest LTS version of Node.js or greater](https://nodejs.org/en/) + +## Installing and importing Dapr's JS SDK + +1. Install the SDK with `npm`: + +```bash +npm i @dapr/dapr --save +``` + +2. Import the libraries: + +```typescript +import { DaprClient, DaprServer, HttpMethod, CommunicationProtocolEnum } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; // Dapr Sidecar Host +const daprPort = "3500"; // Dapr Sidecar Port of this Example Server +const serverHost = "127.0.0.1"; // App Host of this Example Server +const serverPort = "50051"; // App Port of this Example Server + +// HTTP Example +const client = new DaprClient({ daprHost, daprPort }); + +// GRPC Example +const client = new DaprClient({ daprHost, daprPort, communicationProtocol: CommunicationProtocolEnum.GRPC }); +``` + +## Running + +To run the examples, you can use two different protocols to interact with the Dapr sidecar: HTTP (default) or gRPC. + +### Using HTTP (default) + +```typescript +import { DaprClient } from "@dapr/dapr"; +const client = new DaprClient({ daprHost, daprPort }); +``` + +```bash +# Using dapr run +dapr run --app-id example-sdk --app-protocol http -- npm run start + +# or, using npm script +npm run start:dapr-http +``` + +### Using gRPC + +Since HTTP is the default, you will have to adapt the communication protocol to use gRPC. You can do this by passing an extra argument to the client or server constructor. + +```typescript +import { DaprClient, CommunicationProtocol } from "@dapr/dapr"; +const client = new DaprClient({ daprHost, daprPort, communicationProtocol: CommunicationProtocol.GRPC }); +``` + +```bash +# Using dapr run +dapr run --app-id example-sdk --app-protocol grpc -- npm run start + +# or, using npm script +npm run start:dapr-grpc +``` + +### Environment Variables + +##### Dapr Sidecar Endpoints + +You can use the `DAPR_HTTP_ENDPOINT` and `DAPR_GRPC_ENDPOINT` environment variables to set the Dapr +Sidecar's HTTP and gRPC endpoints respectively. When these variables are set, the `daprHost` +and `daprPort` don't have to be set in the options argument of the constructor, the client will parse them automatically +out of the provided endpoints. + +```typescript +import { DaprClient, CommunicationProtocol } from "@dapr/dapr"; + +// Using HTTP, when DAPR_HTTP_ENDPOINT is set +const client = new DaprClient(); + +// Using gRPC, when DAPR_GRPC_ENDPOINT is set +const client = new DaprClient({ communicationProtocol: CommunicationProtocol.GRPC }); +``` + +If the environment variables are set, but `daprHost` and `daprPort` values are passed to the +constructor, the latter will take precedence over the environment variables. + +##### Dapr API Token + +You can use the `DAPR_API_TOKEN` environment variable to set the Dapr API token. When this variable +is set, the `daprApiToken` doesn't have to be set in the options argument of the constructor, +the client will get it automatically. + +## General + +### Increasing Body Size + +You can increase the body size that is used by the application to communicate with the sidecar by using a`DaprClient`'s option. + +```typescript +import { DaprClient, CommunicationProtocol } from "@dapr/dapr"; + +// Allow a body size of 10Mb to be used +// The default is 4Mb +const client = new DaprClient({ + daprHost, + daprPort, + communicationProtocol: CommunicationProtocol.HTTP, + maxBodySizeMb: 10, +}); +``` + +### Proxying Requests + +By proxying requests, we can utilize the unique capabilities that Dapr brings with its sidecar architecture such as service discovery, logging, etc., enabling us to instantly "upgrade" our gRPC services. This feature of gRPC proxying was demonstrated in [community call 41](https://www.youtube.com/watch?v=B_vkXqptpXY&t=71s). + +#### Creating a Proxy + +To perform gRPC proxying, simply create a proxy by calling the `client.proxy.create()` method: + +```typescript +// As always, create a client to our dapr sidecar +// this client takes care of making sure the sidecar is started, that we can communicate, ... +const clientSidecar = new DaprClient({ daprHost, daprPort, communicationProtocol: CommunicationProtocol.GRPC }); + +// Create a Proxy that allows us to use our gRPC code +const clientProxy = await clientSidecar.proxy.create(GreeterClient); +``` + +We can now call the methods as defined in our `GreeterClient` interface (which in this case is from the [Hello World example](https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld.proto)) + +#### Behind the Scenes (Technical Working) + +![Architecture](assets/architecture.png) + +1. The gRPC service gets started in Dapr. We tell Dapr which port this gRPC server is running on through `--app-port` and give it a unique Dapr app ID with `--app-id ` +2. We can now call the Dapr Sidecar through a client that will connect to the Sidecar +3. Whilst calling the Dapr Sidecar, we provide a metadata key named `dapr-app-id` with the value of our gRPC server booted in Dapr (e.g. `server` in our example) +4. Dapr will now forward the call to the gRPC server configured + +## Building blocks + +The JavaScript Client SDK allows you to interface with all of the [Dapr building blocks]({{% ref building-blocks %}}) focusing on Client to Sidecar features. + +### Invocation API + +#### Invoke a Service + +```typescript +import { DaprClient, HttpMethod } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; +const daprPort = "3500"; + +async function start() { + const client = new DaprClient({ daprHost, daprPort }); + + const serviceAppId = "my-app-id"; + const serviceMethod = "say-hello"; + + // POST Request + const response = await client.invoker.invoke(serviceAppId, serviceMethod, HttpMethod.POST, { hello: "world" }); + + // POST Request with headers + const response = await client.invoker.invoke( + serviceAppId, + serviceMethod, + HttpMethod.POST, + { hello: "world" }, + { headers: { "X-User-ID": "123" } }, + ); + + // GET Request + const response = await client.invoker.invoke(serviceAppId, serviceMethod, HttpMethod.GET); +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); +``` + +> For a full guide on service invocation visit [How-To: Invoke a service]({{% ref howto-invoke-discover-services.md %}}). + +### State Management API + +#### Save, Get and Delete application state + +```typescript +import { DaprClient } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; +const daprPort = "3500"; + +async function start() { + const client = new DaprClient({ daprHost, daprPort }); + + const serviceStoreName = "my-state-store-name"; + + // Save State + const response = await client.state.save( + serviceStoreName, + [ + { + key: "first-key-name", + value: "hello", + metadata: { + foo: "bar", + }, + }, + { + key: "second-key-name", + value: "world", + }, + ], + { + metadata: { + ttlInSeconds: "3", // this should override the ttl in the state item + }, + }, + ); + + // Get State + const response = await client.state.get(serviceStoreName, "first-key-name"); + + // Get Bulk State + const response = await client.state.getBulk(serviceStoreName, ["first-key-name", "second-key-name"]); + + // State Transactions + await client.state.transaction(serviceStoreName, [ + { + operation: "upsert", + request: { + key: "first-key-name", + value: "new-data", + }, + }, + { + operation: "delete", + request: { + key: "second-key-name", + }, + }, + ]); + + // Delete State + const response = await client.state.delete(serviceStoreName, "first-key-name"); +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); +``` + +> For a full list of state operations visit [How-To: Get & save state]({{% ref howto-get-save-state.md %}}). + +#### Query State API + +```typescript +import { DaprClient } from "@dapr/dapr"; + +async function start() { + const client = new DaprClient({ daprHost, daprPort }); + + const res = await client.state.query("state-mongodb", { + filter: { + OR: [ + { + EQ: { "person.org": "Dev Ops" }, + }, + { + AND: [ + { + EQ: { "person.org": "Finance" }, + }, + { + IN: { state: ["CA", "WA"] }, + }, + ], + }, + ], + }, + sort: [ + { + key: "state", + order: "DESC", + }, + ], + page: { + limit: 10, + }, + }); + + console.log(res); +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); +``` + +### PubSub API + +#### Publish messages + +```typescript +import { DaprClient } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; +const daprPort = "3500"; + +async function start() { + const client = new DaprClient({ daprHost, daprPort }); + + const pubSubName = "my-pubsub-name"; + const topic = "topic-a"; + + // Publish message to topic as text/plain + // Note, the content type is inferred from the message type unless specified explicitly + const response = await client.pubsub.publish(pubSubName, topic, "hello, world!"); + // If publish fails, response contains the error + console.log(response); + + // Publish message to topic as application/json + await client.pubsub.publish(pubSubName, topic, { hello: "world" }); + + // Publish a JSON message as plain text + const options = { contentType: "text/plain" }; + await client.pubsub.publish(pubSubName, topic, { hello: "world" }, options); + + // Publish message to topic as application/cloudevents+json + // You can also use the cloudevent SDK to create cloud events https://github.com/cloudevents/sdk-javascript + const cloudEvent = { + specversion: "1.0", + source: "/some/source", + type: "example", + id: "1234", + }; + await client.pubsub.publish(pubSubName, topic, cloudEvent); + + // Publish a cloudevent as raw payload + const options = { metadata: { rawPayload: true } }; + await client.pubsub.publish(pubSubName, topic, "hello, world!", options); + + // Publish multiple messages to a topic as text/plain + await client.pubsub.publishBulk(pubSubName, topic, ["message 1", "message 2", "message 3"]); + + // Publish multiple messages to a topic as application/json + await client.pubsub.publishBulk(pubSubName, topic, [ + { hello: "message 1" }, + { hello: "message 2" }, + { hello: "message 3" }, + ]); + + // Publish multiple messages with explicit bulk publish messages + const bulkPublishMessages = [ + { + entryID: "entry-1", + contentType: "application/json", + event: { hello: "foo message 1" }, + }, + { + entryID: "entry-2", + contentType: "application/cloudevents+json", + event: { ...cloudEvent, data: "foo message 2", datacontenttype: "text/plain" }, + }, + { + entryID: "entry-3", + contentType: "text/plain", + event: "foo message 3", + }, + ]; + await client.pubsub.publishBulk(pubSubName, topic, bulkPublishMessages); +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); +``` + +### Bindings API + +#### Invoke Output Binding + +**Output Bindings** + +```typescript +import { DaprClient } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; +const daprPort = "3500"; + +async function start() { + const client = new DaprClient({ daprHost, daprPort }); + + const bindingName = "my-binding-name"; + const bindingOperation = "create"; + const message = { hello: "world" }; + + const response = await client.binding.send(bindingName, bindingOperation, message); +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); +``` + +> For a full guide on output bindings visit [How-To: Use bindings]({{% ref howto-bindings.md %}}). + +### Secret API + +#### Retrieve secrets + +```typescript +import { DaprClient } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; +const daprPort = "3500"; + +async function start() { + const client = new DaprClient({ daprHost, daprPort }); + + const secretStoreName = "my-secret-store"; + const secretKey = "secret-key"; + + // Retrieve a single secret from secret store + const response = await client.secret.get(secretStoreName, secretKey); + + // Retrieve all secrets from secret store + const response = await client.secret.getBulk(secretStoreName); +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); +``` + +> For a full guide on secrets visit [How-To: Retrieve secrets]({{% ref howto-secrets.md %}}). + +### Configuration API + +#### Get Configuration Keys + +```typescript +import { DaprClient } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; + +async function start() { + const client = new DaprClient({ + daprHost, + daprPort: process.env.DAPR_GRPC_PORT, + communicationProtocol: CommunicationProtocolEnum.GRPC, + }); + + const config = await client.configuration.get("config-store", ["key1", "key2"]); + console.log(config); +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); +``` + +Sample output: + +```log +{ + items: { + key1: { key: 'key1', value: 'foo', version: '', metadata: {} }, + key2: { key: 'key2', value: 'bar2', version: '', metadata: {} } + } +} +``` + +#### Subscribe to Configuration Updates + +```typescript +import { DaprClient } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; + +async function start() { + const client = new DaprClient({ + daprHost, + daprPort: process.env.DAPR_GRPC_PORT, + communicationProtocol: CommunicationProtocolEnum.GRPC, + }); + + // Subscribes to config store changes for keys "key1" and "key2" + const stream = await client.configuration.subscribeWithKeys("config-store", ["key1", "key2"], async (data) => { + console.log("Subscribe received updates from config store: ", data); + }); + + // Wait for 60 seconds and unsubscribe. + await new Promise((resolve) => setTimeout(resolve, 60000)); + stream.stop(); +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); +``` + +Sample output: + +```log +Subscribe received updates from config store: { + items: { key2: { key: 'key2', value: 'bar', version: '', metadata: {} } } +} +Subscribe received updates from config store: { + items: { key1: { key: 'key1', value: 'foobar', version: '', metadata: {} } } +} +``` + +### Cryptography API + +> Support for the cryptography API is only available on the gRPC client in the JavaScript SDK. + +```typescript +import { createReadStream, createWriteStream } from "node:fs"; +import { readFile, writeFile } from "node:fs/promises"; +import { pipeline } from "node:stream/promises"; + +import { DaprClient, CommunicationProtocolEnum } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; +const daprPort = "50050"; // Dapr Sidecar Port of this example server + +async function start() { + const client = new DaprClient({ + daprHost, + daprPort, + communicationProtocol: CommunicationProtocolEnum.GRPC, + }); + + // Encrypt and decrypt a message using streams + await encryptDecryptStream(client); + + // Encrypt and decrypt a message from a buffer + await encryptDecryptBuffer(client); +} + +async function encryptDecryptStream(client: DaprClient) { + // First, encrypt the message + console.log("== Encrypting message using streams"); + console.log("Encrypting plaintext.txt to ciphertext.out"); + + await pipeline( + createReadStream("plaintext.txt"), + await client.crypto.encrypt({ + componentName: "crypto-local", + keyName: "symmetric256", + keyWrapAlgorithm: "A256KW", + }), + createWriteStream("ciphertext.out"), + ); + + // Decrypt the message + console.log("== Decrypting message using streams"); + console.log("Encrypting ciphertext.out to plaintext.out"); + await pipeline( + createReadStream("ciphertext.out"), + await client.crypto.decrypt({ + componentName: "crypto-local", + }), + createWriteStream("plaintext.out"), + ); +} + +async function encryptDecryptBuffer(client: DaprClient) { + // Read "plaintext.txt" so we have some content + const plaintext = await readFile("plaintext.txt"); + + // First, encrypt the message + console.log("== Encrypting message using buffers"); + + const ciphertext = await client.crypto.encrypt(plaintext, { + componentName: "crypto-local", + keyName: "my-rsa-key", + keyWrapAlgorithm: "RSA", + }); + + await writeFile("test.out", ciphertext); + + // Decrypt the message + console.log("== Decrypting message using buffers"); + const decrypted = await client.crypto.decrypt(ciphertext, { + componentName: "crypto-local", + }); + + // The contents should be equal + if (plaintext.compare(decrypted) !== 0) { + throw new Error("Decrypted message does not match original message"); + } +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); +``` + +> For a full guide on cryptography visit [How-To: Cryptography]({{% ref howto-cryptography.md %}}). + +### Distributed Lock API + +#### Try Lock and Unlock APIs + +```typescript +import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr"; +import { LockStatus } from "@dapr/dapr/types/lock/UnlockResponse"; + +const daprHost = "127.0.0.1"; +const daprPortDefault = "3500"; + +async function start() { + const client = new DaprClient({ daprHost, daprPort }); + + const storeName = "redislock"; + const resourceId = "resourceId"; + const lockOwner = "owner1"; + let expiryInSeconds = 1000; + + console.log(`Acquiring lock on ${storeName}, ${resourceId} as owner: ${lockOwner}`); + const lockResponse = await client.lock.lock(storeName, resourceId, lockOwner, expiryInSeconds); + console.log(lockResponse); + + console.log(`Unlocking on ${storeName}, ${resourceId} as owner: ${lockOwner}`); + const unlockResponse = await client.lock.unlock(storeName, resourceId, lockOwner); + console.log("Unlock API response: " + getResponseStatus(unlockResponse.status)); +} + +function getResponseStatus(status: LockStatus) { + switch (status) { + case LockStatus.Success: + return "Success"; + case LockStatus.LockDoesNotExist: + return "LockDoesNotExist"; + case LockStatus.LockBelongsToOthers: + return "LockBelongsToOthers"; + default: + return "InternalError"; + } +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); +``` + +> For a full guide on distributed locks visit [How-To: Use Distributed Locks]({{% ref howto-use-distributed-lock.md %}}). + +### Workflow API + +#### Workflow management + +```typescript +import { DaprClient } from "@dapr/dapr"; + +async function start() { + const client = new DaprClient(); + + // Start a new workflow instance + const instanceId = await client.workflow.start("OrderProcessingWorkflow", { + Name: "Paperclips", + TotalCost: 99.95, + Quantity: 4, + }); + console.log(`Started workflow instance ${instanceId}`); + + // Get a workflow instance + const workflow = await client.workflow.get(instanceId); + console.log( + `Workflow ${workflow.workflowName}, created at ${workflow.createdAt.toUTCString()}, has status ${ + workflow.runtimeStatus + }`, + ); + console.log(`Additional properties: ${JSON.stringify(workflow.properties)}`); + + // Pause a workflow instance + await client.workflow.pause(instanceId); + console.log(`Paused workflow instance ${instanceId}`); + + // Resume a workflow instance + await client.workflow.resume(instanceId); + console.log(`Resumed workflow instance ${instanceId}`); + + // Terminate a workflow instance + await client.workflow.terminate(instanceId); + console.log(`Terminated workflow instance ${instanceId}`); + + // Purge a workflow instance + await client.workflow.purge(instanceId); + console.log(`Purged workflow instance ${instanceId}`); +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); +``` + +## Related links + +- [JavaScript SDK examples](https://github.com/dapr/js-sdk/tree/master/examples) diff --git a/sdkdocs/js/content/en/js-sdk-docs/js-examples/_index.md b/sdkdocs/js/content/en/js-sdk-docs/js-examples/_index.md new file mode 100644 index 00000000000..4ac669379e7 --- /dev/null +++ b/sdkdocs/js/content/en/js-sdk-docs/js-examples/_index.md @@ -0,0 +1,30 @@ +--- +type: docs +title: "JavaScript Examples" +linkTitle: "Examples" +weight: 5000 +description: Get started with the Dapr Javascript SDK through some of our examples! +--- + +## Quickstarts + +- [State Management](https://github.com/dapr/quickstarts/tree/master/state_management/javascript/sdk): Learn the concept of state management with Dapr +- [Pub Sub](https://github.com/dapr/quickstarts/tree/master/pub_sub/javascript/sdk): Create your own Publish / Subscribe system +- [Secrets Management](https://github.com/dapr/quickstarts/tree/master/secrets_management/javascript/sdk) +- [Service Invocation](https://github.com/dapr/quickstarts/tree/master/service_invocation/javascript/http) + +## Articles + +> Want your article added? [Let us know!](https://github.com/dapr/js-sdk/discussions/categories/articles) so we can add it below + +- [xaviergeerinck.com - Create an Azure IoT Hub Stream Processor with Dapr](https://xaviergeerinck.com/2022/05/19/create-an-azure-iot-hub-stream-processor-with-dapr/) + +- [xaviergeerinck.com - Integrate Dapr with Nest.JS and the Dapr JS SDK](https://xaviergeerinck.com/2022/03/29/integrate-dapr-with-nest-js-and-the-dapr-js-sdk/) + +- [xaviergeerinck.com - Parking Garage Sensor implementation using Dapr Actors](https://xaviergeerinck.com/2021/10/09/parking-garage-sensor-implementation-using-dapr-actors/) + +- [xaviergeerinck.com - Running Dapr on Azure IoT Edge](https://xaviergeerinck.com/2021/04/23/running-dapr-on-azure-iot-edge/) + +- [xaviergeerinck.com - Tutorial - Creating an Email Microservice with Typescript and Dapr](https://xaviergeerinck.com/2021/03/25/tutorial---creating-an-email-microservice-with-typescript-and-dapr/) + +- [xaviergeerinck.com - Dapr - Creating a User Login/Register Microservice](https://xaviergeerinck.com/2020/04/10/dapr---creating-a-user-login-register-microservice/) diff --git a/sdkdocs/js/content/en/js-sdk-docs/js-logger/_index.md b/sdkdocs/js/content/en/js-sdk-docs/js-logger/_index.md new file mode 100644 index 00000000000..4ad22272371 --- /dev/null +++ b/sdkdocs/js/content/en/js-sdk-docs/js-logger/_index.md @@ -0,0 +1,106 @@ +--- +type: docs +title: "Logging in JavaScript SDK" +linkTitle: "Logging" +weight: 4000 +description: Configuring logging in JavaScript SDK +--- + +## Introduction + +The JavaScript SDK comes with a out-of-box `Console` based logger. The SDK emits various internal logs to help users understand the chain of events and troubleshoot problems. A consumer of this SDK can customize the verbosity of the log, as well as provide their own implementation for the logger. + +## Configure log level + +There are five levels of logging in **descending order of importance** - `error`, `warn`, `info`, `verbose`, and `debug`. Setting the log to a level means that the logger will emit all the logs that are at least as important as the mentioned level. For example, setting to `verbose` log means that the SDK will not emit `debug` level logs. The default log level is `info`. + +### Dapr Client + +```js +import { CommunicationProtocolEnum, DaprClient, LogLevel } from "@dapr/dapr"; + +// create a client instance with log level set to verbose. +const client = new DaprClient({ + daprHost, + daprPort, + communicationProtocol: CommunicationProtocolEnum.HTTP, + logger: { level: LogLevel.Verbose }, +}); +``` + +> For more details on how to use the Client, see [JavaScript Client]({{% ref js-client %}}). + +### DaprServer + +```ts +import { CommunicationProtocolEnum, DaprServer, LogLevel } from "@dapr/dapr"; + +// create a server instance with log level set to error. +const server = new DaprServer({ + serverHost, + serverPort, + clientOptions: { + daprHost, + daprPort, + logger: { level: LogLevel.Error }, + }, +}); +``` + +> For more details on how to use the Server, see [JavaScript Server]({{% ref js-server %}}). + +## Custom LoggerService + +The JavaScript SDK uses the in-built `Console` for logging. To use a custom logger like Winston or Pino, you can implement the `LoggerService` interface. + +### Winston based logging: + +Create a new implementation of `LoggerService`. + +```ts +import { LoggerService } from "@dapr/dapr"; +import * as winston from "winston"; + +export class WinstonLoggerService implements LoggerService { + private logger; + + constructor() { + this.logger = winston.createLogger({ + transports: [new winston.transports.Console(), new winston.transports.File({ filename: "combined.log" })], + }); + } + + error(message: any, ...optionalParams: any[]): void { + this.logger.error(message, ...optionalParams); + } + warn(message: any, ...optionalParams: any[]): void { + this.logger.warn(message, ...optionalParams); + } + info(message: any, ...optionalParams: any[]): void { + this.logger.info(message, ...optionalParams); + } + verbose(message: any, ...optionalParams: any[]): void { + this.logger.verbose(message, ...optionalParams); + } + debug(message: any, ...optionalParams: any[]): void { + this.logger.debug(message, ...optionalParams); + } +} +``` + +Pass the new implementation to the SDK. + +```ts +import { CommunicationProtocolEnum, DaprClient, LogLevel } from "@dapr/dapr"; +import { WinstonLoggerService } from "./WinstonLoggerService"; + +const winstonLoggerService = new WinstonLoggerService(); + +// create a client instance with log level set to verbose and logger service as winston. +const client = new DaprClient({ + daprHost, + daprPort, + communicationProtocol: CommunicationProtocolEnum.HTTP, + logger: { level: LogLevel.Verbose, service: winstonLoggerService }, +}); +``` diff --git a/sdkdocs/js/content/en/js-sdk-docs/js-server/_index.md b/sdkdocs/js/content/en/js-sdk-docs/js-server/_index.md new file mode 100644 index 00000000000..462ff4b81bc --- /dev/null +++ b/sdkdocs/js/content/en/js-sdk-docs/js-server/_index.md @@ -0,0 +1,665 @@ +--- +type: docs +title: "JavaScript Server SDK" +linkTitle: "Server" +weight: 2000 +description: JavaScript Server SDK for developing Dapr applications +--- + +## Introduction + +The Dapr Server will allow you to receive communication from the Dapr Sidecar and get access to its server facing features such as: Subscribing to Events, Receiving Input Bindings, and much more. + +## Pre-requisites + +- [Dapr CLI]({{% ref install-dapr-cli.md %}}) installed +- Initialized [Dapr environment]({{% ref install-dapr-selfhost.md %}}) +- [Latest LTS version of Node or greater](https://nodejs.org/en/) + +## Installing and importing Dapr's JS SDK + +1. Install the SDK with `npm`: + +```bash +npm i @dapr/dapr --save +``` + +2. Import the libraries: + +```typescript +import { DaprServer, CommunicationProtocolEnum } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; // Dapr Sidecar Host +const daprPort = "3500"; // Dapr Sidecar Port of this Example Server +const serverHost = "127.0.0.1"; // App Host of this Example Server +const serverPort = "50051"; // App Port of this Example Server + +// HTTP Example +const server = new DaprServer({ + serverHost, + serverPort, + communicationProtocol: CommunicationProtocolEnum.HTTP, // DaprClient to use same communication protocol as DaprServer, in case DaprClient protocol not mentioned explicitly + clientOptions: { + daprHost, + daprPort, + }, +}); + +// GRPC Example +const server = new DaprServer({ + serverHost, + serverPort, + communicationProtocol: CommunicationProtocolEnum.GRPC, + clientOptions: { + daprHost, + daprPort, + }, +}); +``` + +## Running + +To run the examples, you can use two different protocols to interact with the Dapr sidecar: HTTP (default) or gRPC. + +### Using HTTP (built-in express webserver) + +```typescript +import { DaprServer } from "@dapr/dapr"; + +const server = new DaprServer({ + serverHost: appHost, + serverPort: appPort, + clientOptions: { + daprHost, + daprPort, + }, +}); +// initialize subscribtions, ... before server start +// the dapr sidecar relies on these +await server.start(); +``` + +```bash +# Using dapr run +dapr run --app-id example-sdk --app-port 50051 --app-protocol http -- npm run start + +# or, using npm script +npm run start:dapr-http +``` + +> ℹ️ **Note:** The `app-port` is required here, as this is where our server will need to bind to. Dapr will check for the application to bind to this port, before finishing start-up. + +### Using HTTP (bring your own express webserver) + +Instead of using the built-in web server for Dapr sidecar to application communication, you can also bring your own instance. This is helpful in scenarios like when you are building a REST API back-end and want to integrate Dapr directly in it. + +Note, this is currently available for [`express`](https://www.npmjs.com/package/express) only. + +> 💡 Note: when using a custom web-server, the SDK will configure server properties like max body size, and add new routes to it. The routes are unique on their own to avoid any collisions with your application, but it's not guaranteed to not collide. + +```typescript +import { DaprServer, CommunicationProtocolEnum } from "@dapr/dapr"; +import express from "express"; + +const myApp = express(); + +myApp.get("/my-custom-endpoint", (req, res) => { + res.send({ msg: "My own express app!" }); +}); + +const daprServer = new DaprServer({ + serverHost: "127.0.0.1", // App Host + serverPort: "50002", // App Port + serverHttp: myApp, + clientOptions: { + daprHost + daprPort + } + }); + +// Initialize subscriptions before the server starts, the Dapr sidecar uses it. +// This will also initialize the app server itself (removing the need for `app.listen` to be called). +await daprServer.start(); +``` + +After configuring the above, you can call your custom endpoint as you normally would: + +```typescript +const res = await fetch(`http://127.0.0.1:50002/my-custom-endpoint`); +const json = await res.json(); +``` + +### Using gRPC + +Since HTTP is the default, you will have to adapt the communication protocol to use gRPC. You can do this by passing an extra argument to the client or server constructor. + +```typescript +import { DaprServer, CommunicationProtocol } from "@dapr/dapr"; + +const server = new DaprServer({ + serverHost: appHost, + serverPort: appPort, + communicationProtocol: CommunicationProtocolEnum.GRPC, + clientOptions: { + daprHost, + daprPort, + }, +}); +// initialize subscribtions, ... before server start +// the dapr sidecar relies on these +await server.start(); +``` + +```bash +# Using dapr run +dapr run --app-id example-sdk --app-port 50051 --app-protocol grpc -- npm run start + +# or, using npm script +npm run start:dapr-grpc +``` + +> ℹ️ **Note:** The `app-port` is required here, as this is where our server will need to bind to. Dapr will check for the application to bind to this port, before finishing start-up. + +## Building blocks + +The JavaScript Server SDK allows you to interface with all of the [Dapr building blocks]({{% ref building-blocks %}}) focusing on Sidecar to App features. + +### Invocation API + +#### Listen to an Invocation + +```typescript +import { DaprServer, DaprInvokerCallbackContent } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; // Dapr Sidecar Host +const daprPort = "3500"; // Dapr Sidecar Port of this Example Server +const serverHost = "127.0.0.1"; // App Host of this Example Server +const serverPort = "50051"; // App Port of this Example Server " + +async function start() { + const server = new DaprServer({ + serverHost, + serverPort, + clientOptions: { + daprHost, + daprPort, + }, + }); + + const callbackFunction = (data: DaprInvokerCallbackContent) => { + console.log("Received body: ", data.body); + console.log("Received metadata: ", data.metadata); + console.log("Received query: ", data.query); + console.log("Received headers: ", data.headers); // only available in HTTP + }; + + await server.invoker.listen("hello-world", callbackFunction, { method: HttpMethod.GET }); + + // You can now invoke the service with your app id and method "hello-world" + + await server.start(); +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); +``` + +> For a full guide on service invocation visit [How-To: Invoke a service]({{% ref howto-invoke-discover-services.md %}}). + +### PubSub API + +#### Subscribe to messages + +Subscribing to messages can be done in several ways to offer flexibility of receiving messages on your topics: + +- Direct subscription through the `subscribe` method +- Direct susbcription with options through the `subscribeWithOptions` method +- Subscription afterwards through the `susbcribeOnEvent` method + +Each time an event arrives, we pass its body as `data` and the headers as `headers`, which can contain properties of the event publisher (e.g., a device ID from IoT Hub) + +> Dapr requires subscriptions to be set up on startup, but in the JS SDK we allow event handlers to be added afterwards as well, providing you the flexibility of programming. + +An example is provided below + +```typescript +import { DaprServer } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; // Dapr Sidecar Host +const daprPort = "3500"; // Dapr Sidecar Port of this Example Server +const serverHost = "127.0.0.1"; // App Host of this Example Server +const serverPort = "50051"; // App Port of this Example Server " + +async function start() { + const server = new DaprServer({ + serverHost, + serverPort, + clientOptions: { + daprHost, + daprPort, + }, + }); + + const pubSubName = "my-pubsub-name"; + const topic = "topic-a"; + + // Configure Subscriber for a Topic + // Method 1: Direct subscription through the `subscribe` method + await server.pubsub.subscribe(pubSubName, topic, async (data: any, headers: object) => + console.log(`Received Data: ${JSON.stringify(data)} with headers: ${JSON.stringify(headers)}`), + ); + + // Method 2: Direct susbcription with options through the `subscribeWithOptions` method + await server.pubsub.subscribeWithOptions(pubSubName, topic, { + callback: async (data: any, headers: object) => + console.log(`Received Data: ${JSON.stringify(data)} with headers: ${JSON.stringify(headers)}`), + }); + + // Method 3: Subscription afterwards through the `susbcribeOnEvent` method + // Note: we use default, since if no route was passed (empty options) we utilize "default" as the route name + await server.pubsub.subscribeWithOptions("pubsub-redis", "topic-options-1", {}); + server.pubsub.subscribeToRoute("pubsub-redis", "topic-options-1", "default", async (data: any, headers: object) => { + console.log(`Received Data: ${JSON.stringify(data)} with headers: ${JSON.stringify(headers)}`); + }); + + // Start the server + await server.start(); +} +``` + +> For a full list of state operations visit [How-To: Publish & subscribe]({{% ref howto-publish-subscribe.md %}}). + +#### Subscribe with SUCCESS/RETRY/DROP status + +Dapr supports [status codes for retry logic](https://docs.dapr.io/reference/api/pubsub_api/#expected-http-response) to specify what should happen after a message gets processed. + +> ⚠️ The JS SDK allows multiple callbacks on the same topic, we handle priority of status on `RETRY` > `DROP` > `SUCCESS` and default to `SUCCESS` + +> ⚠️ Make sure to [configure resiliency](https://docs.dapr.io/operations/resiliency/resiliency-overview/) in your application to handle `RETRY` messages + +In the JS SDK we support these messages through the `DaprPubSubStatusEnum` enum. To ensure Dapr will retry we configure a Resiliency policy as well. + +**components/resiliency.yaml** + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Resiliency +metadata: + name: myresiliency +spec: + policies: + retries: + # Global Retry Policy for Inbound Component operations + DefaultComponentInboundRetryPolicy: + policy: constant + duration: 500ms + maxRetries: 10 + targets: + components: + messagebus: + inbound: + retry: DefaultComponentInboundRetryPolicy +``` + +**src/index.ts** + +```typescript +import { DaprServer, DaprPubSubStatusEnum } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; // Dapr Sidecar Host +const daprPort = "3500"; // Dapr Sidecar Port of this Example Server +const serverHost = "127.0.0.1"; // App Host of this Example Server +const serverPort = "50051"; // App Port of this Example Server " + +async function start() { + const server = new DaprServer({ + serverHost, + serverPort, + clientOptions: { + daprHost, + daprPort, + }, + }); + + const pubSubName = "my-pubsub-name"; + const topic = "topic-a"; + + // Process a message successfully + await server.pubsub.subscribe(pubSubName, topic, async (data: any, headers: object) => { + return DaprPubSubStatusEnum.SUCCESS; + }); + + // Retry a message + // Note: this example will keep on retrying to deliver the message + // Note 2: each component can have their own retry configuration + // e.g., https://docs.dapr.io/reference/components-reference/supported-pubsub/setup-redis-pubsub/ + await server.pubsub.subscribe(pubSubName, topic, async (data: any, headers: object) => { + return DaprPubSubStatusEnum.RETRY; + }); + + // Drop a message + await server.pubsub.subscribe(pubSubName, topic, async (data: any, headers: object) => { + return DaprPubSubStatusEnum.DROP; + }); + + // Start the server + await server.start(); +} +``` + +#### Subscribe to messages rule based + +Dapr [supports routing messages](https://docs.dapr.io/developing-applications/building-blocks/pubsub/howto-route-messages/) to different handlers (routes) based on rules. + +> E.g., you are writing an application that needs to handle messages depending on their "type" with Dapr, you can send them to different routes `handlerType1` and `handlerType2` with the default route being `handlerDefault` + +```typescript +import { DaprServer } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; // Dapr Sidecar Host +const daprPort = "3500"; // Dapr Sidecar Port of this Example Server +const serverHost = "127.0.0.1"; // App Host of this Example Server +const serverPort = "50051"; // App Port of this Example Server " + +async function start() { + const server = new DaprServer({ + serverHost, + serverPort, + clientOptions: { + daprHost, + daprPort, + }, + }); + + const pubSubName = "my-pubsub-name"; + const topic = "topic-a"; + + // Configure Subscriber for a Topic with rule set + // Note: the default route and match patterns are optional + await server.pubsub.subscribe("pubsub-redis", "topic-1", { + default: "/default", + rules: [ + { + match: `event.type == "my-type-1"`, + path: "/type-1", + }, + { + match: `event.type == "my-type-2"`, + path: "/type-2", + }, + ], + }); + + // Add handlers for each route + server.pubsub.subscribeToRoute("pubsub-redis", "topic-1", "default", async (data) => { + console.log(`Handling Default`); + }); + server.pubsub.subscribeToRoute("pubsub-redis", "topic-1", "type-1", async (data) => { + console.log(`Handling Type 1`); + }); + server.pubsub.subscribeToRoute("pubsub-redis", "topic-1", "type-2", async (data) => { + console.log(`Handling Type 2`); + }); + + // Start the server + await server.start(); +} +``` + +#### Susbcribe with Wildcards + +The popular wildcards `*` and `+` are supported (make sure to validate if the [pubsub component supports it](https://docs.dapr.io/reference/components-reference/supported-pubsub/)) and can be subscribed to as follows: + +```typescript +import { DaprServer } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; // Dapr Sidecar Host +const daprPort = "3500"; // Dapr Sidecar Port of this Example Server +const serverHost = "127.0.0.1"; // App Host of this Example Server +const serverPort = "50051"; // App Port of this Example Server " + +async function start() { + const server = new DaprServer({ + serverHost, + serverPort, + clientOptions: { + daprHost, + daprPort, + }, + }); + + const pubSubName = "my-pubsub-name"; + + // * Wildcard + await server.pubsub.subscribe(pubSubName, "/events/*", async (data: any, headers: object) => + console.log(`Received Data: ${JSON.stringify(data)}`), + ); + + // + Wildcard + await server.pubsub.subscribe(pubSubName, "/events/+/temperature", async (data: any, headers: object) => + console.log(`Received Data: ${JSON.stringify(data)}`), + ); + + // Start the server + await server.start(); +} +``` + +#### Bulk Subscribe to messages + +Bulk Subscription is supported and is available through following API: + +- Bulk subscription through the `subscribeBulk` method: `maxMessagesCount` and `maxAwaitDurationMs` are optional; and if not provided, default values for related components will be used. + +While listening for messages, the application receives messages from Dapr in bulk. However, like regular subscribe, the callback function receives a single message at a time, and the user can choose to return a `DaprPubSubStatusEnum` value to acknowledge successfully, retry, or drop the message. The default behavior is to return a success response. + +Please refer [this document](https://v1-10.docs.dapr.io/developing-applications/building-blocks/pubsub/pubsub-bulk/) for more details. + +```typescript +import { DaprServer } from "@dapr/dapr"; + +const pubSubName = "orderPubSub"; +const topic = "topicbulk"; + +const daprHost = process.env.DAPR_HOST || "127.0.0.1"; +const daprHttpPort = process.env.DAPR_HTTP_PORT || "3502"; +const serverHost = process.env.SERVER_HOST || "127.0.0.1"; +const serverPort = process.env.APP_PORT || 5001; + +async function start() { + const server = new DaprServer({ + serverHost, + serverPort, + clientOptions: { + daprHost, + daprPort: daprHttpPort, + }, + }); + + // Publish multiple messages to a topic with default config. + await client.pubsub.subscribeBulk(pubSubName, topic, (data) => + console.log("Subscriber received: " + JSON.stringify(data)), + ); + + // Publish multiple messages to a topic with specific maxMessagesCount and maxAwaitDurationMs. + await client.pubsub.subscribeBulk( + pubSubName, + topic, + (data) => { + console.log("Subscriber received: " + JSON.stringify(data)); + return DaprPubSubStatusEnum.SUCCESS; // If App doesn't return anything, the default is SUCCESS. App can also return RETRY or DROP based on the incoming message. + }, + { + maxMessagesCount: 100, + maxAwaitDurationMs: 40, + }, + ); +} +``` + +#### Dead Letter Topics + +Dapr supports [dead letter topic](https://docs.dapr.io/developing-applications/building-blocks/pubsub/pubsub-deadletter/). This means that when a message fails to be processed, it gets sent to a dead letter queue. E.g., when a message fails to be handled on `/my-queue` it will be sent to `/my-queue-failed`. +E.g., when a message fails to be handled on `/my-queue` it will be sent to `/my-queue-failed`. + +You can use the following options with `subscribeWithOptions` method: + +- `deadletterTopic`: Specify a deadletter topic name (note: if none is provided we create one named `deadletter`) +- `deadletterCallback`: The method to trigger as handler for our deadletter + +Implementing Deadletter support in the JS SDK can be done by either + +- Passing the `deadletterCallback` as an option +- By subscribing to route manually with `subscribeToRoute` + +An example is provided below + +```typescript +import { DaprServer } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; // Dapr Sidecar Host +const daprPort = "3500"; // Dapr Sidecar Port of this Example Server +const serverHost = "127.0.0.1"; // App Host of this Example Server +const serverPort = "50051"; // App Port of this Example Server " + +async function start() { + const server = new DaprServer({ + serverHost, + serverPort, + clientOptions: { + daprHost, + daprPort, + }, + }); + + const pubSubName = "my-pubsub-name"; + + // Method 1 (direct subscribing through subscribeWithOptions) + await server.pubsub.subscribeWithOptions("pubsub-redis", "topic-options-5", { + callback: async (data: any) => { + throw new Error("Triggering Deadletter"); + }, + deadLetterCallback: async (data: any) => { + console.log("Handling Deadletter message"); + }, + }); + + // Method 2 (subscribe afterwards) + await server.pubsub.subscribeWithOptions("pubsub-redis", "topic-options-1", { + deadletterTopic: "my-deadletter-topic", + }); + server.pubsub.subscribeToRoute("pubsub-redis", "topic-options-1", "default", async () => { + throw new Error("Triggering Deadletter"); + }); + server.pubsub.subscribeToRoute("pubsub-redis", "topic-options-1", "my-deadletter-topic", async () => { + console.log("Handling Deadletter message"); + }); + + // Start server + await server.start(); +} +``` + +### Bindings API + +#### Receive an Input Binding + +```typescript +import { DaprServer } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; +const daprPort = "3500"; +const serverHost = "127.0.0.1"; +const serverPort = "5051"; + +async function start() { + const server = new DaprServer({ + serverHost, + serverPort, + clientOptions: { + daprHost, + daprPort, + }, + }); + + const bindingName = "my-binding-name"; + + const response = await server.binding.receive(bindingName, async (data: any) => + console.log(`Got Data: ${JSON.stringify(data)}`), + ); + + await server.start(); +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); +``` + +> For a full guide on output bindings visit [How-To: Use bindings]({{% ref howto-bindings.md %}}). + +### Configuration API + +> 💡 The configuration API is currently only available through gRPC + +#### Getting a configuration value + +```typescript +import { DaprServer } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; +const daprPort = "3500"; +const serverHost = "127.0.0.1"; +const serverPort = "5051"; + +async function start() { + const client = new DaprClient({ + daprHost, + daprPort, + communicationProtocol: CommunicationProtocolEnum.GRPC, + }); + const config = await client.configuration.get("config-redis", ["myconfigkey1", "myconfigkey2"]); +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); +``` + +#### Subscribing to Key Changes + +```typescript +import { DaprServer } from "@dapr/dapr"; + +const daprHost = "127.0.0.1"; +const daprPort = "3500"; +const serverHost = "127.0.0.1"; +const serverPort = "5051"; + +async function start() { + const client = new DaprClient({ + daprHost, + daprPort, + communicationProtocol: CommunicationProtocolEnum.GRPC, + }); + const stream = await client.configuration.subscribeWithKeys("config-redis", ["myconfigkey1", "myconfigkey2"], () => { + // Received a key update + }); + + // When you are ready to stop listening, call the following + await stream.close(); +} + +start().catch((e) => { + console.error(e); + process.exit(1); +}); +``` + +## Related links + +- [JavaScript SDK examples](https://github.com/dapr/js-sdk/tree/main/examples) diff --git a/sdkdocs/js/content/en/js-sdk-docs/js-workflow/_index.md b/sdkdocs/js/content/en/js-sdk-docs/js-workflow/_index.md new file mode 100644 index 00000000000..4cd73009820 --- /dev/null +++ b/sdkdocs/js/content/en/js-sdk-docs/js-workflow/_index.md @@ -0,0 +1,157 @@ +--- +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 +--- + +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 %}})