diff --git a/docs/guide/dubboForWEB/Choosingaprotocol.md b/docs/guide/dubboForWEB/Choosingaprotocol.md index f8e14b92..ae5631af 100644 --- a/docs/guide/dubboForWEB/Choosingaprotocol.md +++ b/docs/guide/dubboForWEB/Choosingaprotocol.md @@ -1 +1,71 @@ # Choosing a protocol + +In addition to the Dubbo protocol, Dubbo ships with support for the gRPC-web protocol. If your backend does not support the Dubbo protocol, you can still use Dubbo clients to interface with it. + +## Connect + +The function `createDubboTransport()` creates a transport for the Dubbo protocol. It uses the [fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) for the actual network operations, which is widely supported in web browsers. The most important options for the Dubbo transport are as follows: + +```ts +import { createDubboTransport } from "@apachedubbo/dubbo-web"; + +const transport = createDubboTransport({ + // Requests will be made to /./method + baseUrl: "http://localhost:8080", + + // By default, this transport uses the JSON format. + // Set this option to true to use the binary format. + useBinaryFormat: false, + + // Controls what the fetch client will do with credentials, such as + // Cookies. The default value is "same-origin", which will not + // transmit Cookies in cross-origin requests. + credentials: "same-origin", + + // Interceptors apply to all calls running through this transport. + interceptors: [], + + // By default, all requests use POST. Set this option to true to use GET + // for side-effect free RPCs. + useHttpGet: false, + + // Optional override of the fetch implementation used by the transport. + fetch: globalThis.fetch; +}); +``` + +We generally recommend the JSON format for web browsers, because it makes it trivial to follow what exactly is sent over the wire with the browser's network inspector. + +Dubbo supports optionally using HTTP GET requests for side-effect free RPC calls, to enable easy use of request caching and more. For more information on HTTP GET requests, see **Get Requests**. + +When creating your transport, you have the option of providing your own custom Fetch function. This can be useful for many different scenarios, such as a overriding or setting Fetch properties as well as working with frameworks such as Svelte that come bundled with their own Fetch implementation. + +It is also possible to configure individual Fetch properties when issuing requests. For some examples of customizing Fetch within Dubbo, take a look at our **Interceptors** docs. In addition, to see how to work with Fetch in an SSR context with frameworks such as Svelte and Nextjs, visit our documentation on **SSR**. + +## gRPC-web + +The function `createGrpcWebTransport()` creates a Transport for the gRPC-web protocol. Any gRPC service can be made available to gRPC-web with the [Envoy Proxy](https://www.envoyproxy.io/). ASP.NET Core supports gRPC-web with a [middleware](https://docs.microsoft.com/en-us/aspnet/core/grpc/browser?view=aspnetcore-6.0). Dubbo for Node and `dubbo-go` support gRPC-web out of the box. + +```ts +import { createGrpcWebTransport } from "@apachedubbo/dubbo-web"; + +const transport = createGrpcWebTransport({ + // Requests will be made to /./method + baseUrl: "http://localhost:8080", + + // By default, this transport uses the binary format, because + // not all gRPC-web implementations support JSON. + useBinaryFormat: true, + + // Controls what the fetch client will do with credentials, such as + // Cookies. The default value is "same-origin", which will not + // transmit Cookies in cross-origin requests. + credentials: "include", + + // Interceptors apply to all calls running through this transport. + interceptors: [], + + // Optional override of the fetch implementation used by the transport. + fetch: globalThis.fetch; +}); +``` \ No newline at end of file diff --git a/docs/guide/dubboForWEB/GeneratingCode.md b/docs/guide/dubboForWEB/GeneratingCode.md index eaa3a3a3..f6b7e13c 100644 --- a/docs/guide/dubboForWEB/GeneratingCode.md +++ b/docs/guide/dubboForWEB/GeneratingCode.md @@ -1,2 +1,125 @@ # Generating code -# todo + +We mentioned earlier that the ELIZA service defines a Protocol Buffer schema. So what *is* that schema? It is really just a simple file that describes the service, its methods, and their argument and return types: + +```protobuf +syntax = "proto3"; + +service ElizaService { + rpc Say(SayRequest) returns (SayResponse) {} +} + +message SayRequest { + string sentence = 1; +} + +message SayResponse { + string sentence = 1; +} +``` + +You can see the full version including comments and some additional RPCs [on the Buf Schema Registry](https://buf.build/connectrpc/eliza/file/main:connectrpc/eliza/v1/eliza.proto) (BSR). The `rpc` keyword stands for Remote Procedure Call — a method you can invoke remotely. The schema is the contract between server and client, and it precisely defines how data is exchanged down to the very details of serialization. + +The schema comes to life by generating code. For the server, an interface is generated, and the developer can focus on filling the methods with business logic. For the client, there really isn't anything to do — the developer can just call the client methods, rely on the generated types for compile-time type-safety and serialization, and focus on the application logic. + +## Generated SDKs + +In the tutorial, we have been using [generated SDKs](https://buf.build/docs/bsr/generated-sdks/npm) with an `npm install` command. When the package was requested on the BSR NPM registry, it ran the schema through a code generator, and served the generated files as a package with all required dependencies. + +If you want to use a Dubbo or gRPC service whose schema is published on the BSR, you can simply use `npm` to install the package, and hit the service with a Dubbo client. + +See our [documentation on generated SDKs](https://buf.build/docs/bsr/generated-sdks/overview) for details. + +## Local generation + +We're going to generate our code using [Buf](https://buf.build/product/cli/), a modern replacement for Google's protobuf compiler, and two compiler plugins: + +- [@apachedubbo/protoc-gen-apache-dubbo-es](https://www.npmjs.com/package/@apachedubbo/protoc-gen-apache-dubbo-es) — generates services from your Protocol Buffer schema +- [@bufbuild/protoc-gen-es](https://www.npmjs.com/package/@bufbuild/protoc-gen-es) — generates base types, like request and response messages + +The code we will generate has three runtime dependencies: + +- [@apachedubbo/dubbo](https://www.npmjs.com/package/@apachedubbo/dubbo) — provides clients, interceptors, errors, and other primitives for Dubbo +- [@apachedubbo/dubbo-web](https://www.npmjs.com/package/@apachedubbo/dubbo-web) — provides the Dubbo and gRPC-web protocols for web browsers +- [@bufbuild/protobuf](https://www.npmjs.com/package/@bufbuild/protobuf) — provides serialization and more for the base types + +First, let's install `buf`, the plugins and runtime dependencies: + +```bash +$ npm install --save-dev @bufbuild/buf @apachedubbo/protoc-gen-apache-dubbo-es @bufbuild/protoc-gen-es +$ npm install @apachedubbo/dubbo @apachedubbo/dubbo @bufbuild/protobuf +``` + +Next, tell Buf to use the two plugins with a new configuration file: + +```yaml +# buf.gen.yaml defines a local generation template. +# For details, see https://buf.build/docs/configuration/v1/buf-gen-yaml +version: v1 +plugins: + # This will invoke protoc-gen-es and write output to src/gen + - plugin: es + out: src/gen + opt: + # Add more plugin options here + - target=ts + # This will invoke protoc-gen-apache-dubbo-es + - plugin: apache-dubbo-es + out: src/gen + opt: + # Add more plugin options here + - target=ts +``` + +If desired, you can also skip local plugin installation and use [remote plugins](https://buf.build/docs/bsr/remote-plugins/overview). + +Finally, tell Buf to generate code for the ELIZA schema: + +```bash +$ npx buf generate buf.build/apache-dubbo/eliza +``` + +If you prefer, you can use `protoc` instead of Buf — the plugins behave like any other plugin. + +### Output + +Let's take a peek at what was generated. There are two new files: + +- `src/gen/apache-dubbo/eliza/v1/eliza_dubbo.ts` +- `src/gen/apache-dubbo/eliza/v1/eliza_pb.ts` + +The first file was generated by `protoc-gen-apache-dubbo-es` and contains the service: + +```ts +import { SayRequest, SayResponse } from "./eliza_pb.js"; +import { MethodKind } from "@bufbuild/protobuf"; + +export const ElizaService = { + typeName: "buf.connect.demo.eliza.v1.ElizaService", + methods: { + say: { + name: "Say", + I: SayRequest, + O: SayResponse, + kind: MethodKind.Unary, + }, + } +} as const; +``` + +The full file includes comments and additional RPCs, but the `const` above really is all Dubbo needs to provide clients. + +The second file was generated by `protoc-gen-es`, and contains the request and response classes. You can see them being imported for the service definition. To learn more about `protoc-gen-es`, head over to the documentation for the [Protobuf-ES project](https://github.com/bufbuild/protobuf-es/blob/main/docs/generated_code.md). + +If your bundler does not handle the `.js` extension in the import `from "./eliza_pb.js"` correctly, you can configure it following our examples, or you can add the plugin option `import_extension=none` to remove the extension. + +You can find the documentation for all available [plugin options on npmjs.com](https://www.npmjs.com/package/@apachedubbo/protoc-gen-apache-dubbo-es#plugin-options) . + +### Using the local files + +To use the locally generated files in the tutorial, update the import path: + +```diff +- import { ElizaService } from "gen/eliza_dubboweb"; ++ import { ElizaService } from "gen/eliza_dubbo"; +``` \ No newline at end of file diff --git a/docs/guide/dubboForWEB/Interceptors.md b/docs/guide/dubboForWEB/Interceptors.md index a4528c4b..a15dee75 100644 --- a/docs/guide/dubboForWEB/Interceptors.md +++ b/docs/guide/dubboForWEB/Interceptors.md @@ -1 +1,164 @@ # Interceptors + +An interceptor can add logic to clients, similar to the decorators or middleware you may have seen in other libraries. Interceptors may mutate the request and response, catch errors and retry/recover, emit logs, or do nearly anything else. + +For a simple example, this interceptor logs all requests: + +```ts +import { Interceptor } from "@apachedubbo/dubbo"; +import { createDubboTransport } from "@apachedubbo/dubbo-web" + +const logger: Interceptor = (next) => async (req) => { + console.log(`sending message to ${req.url}`); + return await next(req); +}; + +createDubboTransport({ + baseUrl: "http://localhost:8080", + interceptors: [logger], +}); +``` +You can think of interceptors like a layered onion. A request initiated by a client goes through the outermost layer first. Each call to `next()` traverses to the next layer. In the center, the actual HTTP request is run by the transport. The response then comes back through all layers and is returned to the client. In the array of interceptors passed to the transport, the interceptor at the end of the array is applied first. + +To intercept responses, we simply look at the return value of `next()`: + +```ts +const logger: Interceptor = (next) => async (req) => { + console.log(`sending message to ${req.url}`); + const res = await next(req); + if (!res.stream) { + console.log("message:", res.message); + } + return res; +}; +``` + +The `stream` property of the response tells us whether this is a streaming response. A streaming response has not fully arrived yet when we intercept it — we have to wrap it to see individual messages: + +```ts +const logger: Interceptor = (next) => async (req) => { + const res = await next(req); + if (res.stream) { + // to intercept streaming response messages, we wrap + // the AsynchronousIterable with a generator function + return { + ...res, + message: logEach(res.message), + }; + } + return res; +}; + +async function* logEach(stream: AsyncIterable) { + for await (const m of stream) { + console.log("message received", m); + yield m; + } +} +``` + +# Context values + +Context values are a type safe way to pass arbitary values from the call site or from one interceptor to the next. You can use `createContextValues` function to create a new `ContextValues`. The `contextValues` call option can be used to provide a `ContextValues` instance for each request. + +`ContextValues` has methods to set, get, and delete values. The keys are `ContextKey` objects: + +## Context Keys + +`ContextKey` is a type safe and collision free way to use context values. It is defined using `createContextKey` function which takes a default value and returns a `ContextKey` object. The default value is used when the context value is not set. + +```ts +import { createContextKey } from "@apachedubbo/dubbo"; + +type User = { name: string }; + +const kUser = createContextKey( + { name: "Anonymous" }, // Default value + { + description: "Current user", // Description useful for debugging + }, +); + +export { kUser }; +``` + +For values where a default doesn't make sense you can just modify the type: + +```ts +import { createContextKey } from "@apachedubbo/dubbo"; + +type User = { name: string }; + +const kUser = createContextKey(undefined, { + description: "Authenticated user", +}); + +export { kUser }; +``` + +It is best to define context keys in a separate file and export them. This is better for code splitting and also avoids circular imports. This also helps in the case where the provider changes based on the environment. + +## Example + +Let's say you want to log the response body. But you don't want to do it for every request. You only want to do it from a specific component. You can use context values to achieve this. + +First create a context key: + +```ts +import { createContextKey } from "@apachedubbo/dubbo"; + +const kLogBody = createContextKey(false, { + description: "Log request/response body", +}); + +export { kLogBody }; +``` + +Then in your interceptor, check the context value: + +```ts +import type { Interceptor } from "@apachedubbo/dubbo"; +import { kLogBody } from "./log-body-context.js"; + +const logger: Interceptor = (next) => async (req) => { + console.log(`sending message to ${req.url}`); + const res = await next(req); + if (!res.stream && req.contextValues.get(kLogBody)) { + console.log("message:", res.message); + } + return res; +}; +``` + +Then in your component, set the context value: + +```ts +import { kLogBody } from "./log-body-context.js"; +import { elizaClient } from "./eliza-client.js"; + +const res = elizaClient.say({ sentence: "Hey!" }, { contextValues: createContextValues().set(kLogBody, true) }); +``` + +# Setting `fetch()` options + +Another valuable use case for interceptors is customizing the Fetch API for individual requests by leveraging the `request.init` object. + +For example, by default, Dubbo sets the Fetch option [redirect](https://developer.mozilla.org/en-US/docs/Web/API/fetch#redirect) to `error`, which means that a network error will be returned when a request is met with a redirect. However, if you wish to change this value to `follow` for example, you can do so using an interceptor. + +```ts +const followRedirects: Interceptor = (next) => async (request) => { + return await next({ + ...request, + init: { + ...request.init, + // Follow all redirects + redirect: "follow", + }, + }); +}; + +const client = createPromiseClient(ElizaService, createConnectTransport({ + baseUrl: "http://localhost:8080", + interceptors: [logger], +})); +``` \ No newline at end of file diff --git a/docs/guide/dubboForWEB/UsingClients.md b/docs/guide/dubboForWEB/UsingClients.md index a8c9d5db..b5e8bc74 100644 --- a/docs/guide/dubboForWEB/UsingClients.md +++ b/docs/guide/dubboForWEB/UsingClients.md @@ -1,2 +1,102 @@ -# Using Clients -# todo +# Using clients + +Dubbo ships with two client shapes for TypeScript, one with classic callbacks, one that is promise based. You create a client for a service using one of the provided constructor functions, and you do not need to generate additional code. + +## Promises + +We have already been using the function `createPromiseClient` in the tutorial. The function gives us a client that uses ECMAScript promise objects. In combination with the `await` keyword, this lets you write asynchronous code in a natural and easily readable way: + +```ts +import { createPromiseClient } from "@apachedubbo/dubbo"; +import { ElizaService } from "../gen/buf/connect/demo/eliza/v1/eliza_dubbo.js"; + +const client = createPromiseClient(ElizaService, transport); + +const res = await client.say({ + sentence: "I feel happy.", +}); +console.log(res.sentence); +``` + +For server-streaming RPCs, the corresponding method on the client will return an async iterable stream of response messages that can be used with the `for await...of` statement: + +```ts +for await (const res of client.introduce({ name: "Joseph" })) { + console.log(res); +} +``` + +## Callbacks + +If you prefer a callback-based approach, the client returned by the function `createCallbackClient` should suit you: + +```ts +import { createCallbackClient } from "@apachedubbo/dubbo"; +import { ElizaService } from "../gen/buf/connect/demo/eliza/v1/eliza_dubbo.js"; + +const client = createCallbackClient(ElizaService, transport); + +client.say({ sentence: "I feel happy." }, (err, res) => { + if (!err) { + console.log(res.sentence); + } +}); +``` + +For server-streaming RPCs, the corresponding method on the client takes two callback functions: one that is called every time a response message arrives, and one that is called at the end of the stream. + +```ts +import {DubboError} from "@apachedubbo/dubbo"; + +client.introduce({name: "Joseph"}, (res) => { + console.log(res); +}, (err?: DubboError) => { + if (err) { + console.error(err); + } +}); +``` + +The callback client is particularly useful if you want to migrate an existing code base from gRPC-web to Connect clients. + +## Managing clients and transports + +In practice, you will likely want to avoid creating a new transport every time you want to use a client. It really depends on the framework of your choice, but there usually is a simple solution to avoid repetition. + +For example, you can easily create a custom hook in React: + +```ts +// use-client.ts +import { useMemo } from "react"; +import { ServiceType } from "@bufbuild/protobuf"; +import { createDubboTransport } from "@apachedubbo/dubbo-web"; +import { createPromiseClient, PromiseClient } from "@apachedubbo/dubbo"; + +// This transport is going to be used throughout the app +const transport = createDubboTransport({ + baseUrl: "http://localhost:8080", +}); + +/** +* Get a promise client for the given service. +*/ +export function useClient(service: T): PromiseClient { + // We memoize the client, so that we only create one instance per service. + return useMemo(() => createPromiseClient(service, transport), [service]); +} +``` + +Usage: + +```ts +await useClient(ElizaService).say({sentence: "I feel happy."}); +``` + +## Roll your own client + +If you find that neither client suits your needs perfectly, it might be an option for you to roll your own. For example, you might prefer [Rust-style result types](https://doc.rust-lang.org/rust-by-example/error/result.html) over promise rejections, and could write your own constructor function that uses the [neverthrow library](https://github.com/supermacro/neverthrow) for method return values. + +Implementing a client constructor takes about 100 lines of code. By convention, every client constructor function must accept call options, and should be named `provideXClient`. + + +