Skip to main content

grpc_webnext_client/
lib.rs

1//! # grpc-webnext-client
2//!
3//! A gRPC client for **Rust WASM frontends**, speaking real gRPC to a
4//! [grpc-webnext](https://github.com/debdattabasu/grpc-webnext) endpoint over an
5//! [h2ts](https://github.com/debdattabasu/h2ts) WebSocket tunnel.
6//!
7//! **No hyper, no tokio — and no tonic unless you ask for it.** The wire is real
8//! HTTP/2 — trailers, multiplexing, flow control — so there is nothing to translate:
9//! framing plus a status read off the trailers is the whole client. Everything is
10//! single-threaded (`Rc`, `!Send`), which is what a browser actually is; nothing here
11//! asserts `Send` to satisfy a runtime that does not exist on this target.
12//!
13//! The optional `tonic` feature runs tonic's *generated* stubs over the same tunnel —
14//! see [Generated stubs](#generated-stubs). It is off by default because it is not
15//! free (≈26 KB gzipped); with it off, this crate has no tonic in its tree at all.
16//!
17//! ```no_run
18//! # async fn demo(client: grpc_webnext_client::Client) -> Result<(), grpc_webnext_client::Status> {
19//! use grpc_webnext_client::CallOptions;
20//!
21//! let reply = client
22//!     .unary("/helloworld.Greeter/SayHello", encoded_request(), CallOptions::new())
23//!     .await?;
24//! let _ = reply.message; // your codec decodes these bytes
25//! # Ok(()) }
26//! # fn encoded_request() -> Vec<u8> { Vec::new() }
27//! ```
28//!
29//! ## Codec
30//!
31//! The client deals in **message bytes**, so it is codec-agnostic: encode with
32//! `prost`, or anything else. The `prost` feature adds typed helpers over
33//! [`prost::Message`], and a service is then a handful of thin wrappers over
34//! [`Client::unary`] and friends.
35//!
36//! ## Generated stubs
37//!
38//! For `greeter.say_hello(request)` instead of hand-written wrappers, the `tonic`
39//! feature lets **tonic's own generated stubs** run over the tunnel —
40//! `GreeterClient::new(client.into_tonic())`, all four cardinalities, nothing
41//! grpc-webnext-specific in the codegen. See [`tonic_service`], which also covers the
42//! one build-script setting a browser target needs.
43//!
44//! ## Streaming
45//!
46//! All four cardinalities. Backpressure on the response is real and costs nothing
47//! here: `h2ts-client` replenishes the HTTP/2 receive window only as the body is
48//! polled, so a consumer that stops reading stops the *server* rather than filling
49//! memory — the same property the TypeScript client gets, for the same reason.
50//!
51//! Dropping a [`Streaming`] cancels the RPC: the HTTP/2 stream is reset, so the
52//! server stops work it has no reader for. That is also how a deadline stops a
53//! stream, and why [`CallOptions::timeout`] covers a stream's whole lifetime rather
54//! than only its opening.
55//!
56//! The streaming cardinalities need `h2ts-client` **0.1.2**: at 0.1.1
57//! `Response::into_body` took `self` while `trailers()` needed `&self`, so a caller
58//! could stream the body or read the trailers, never both — and gRPC's terminal
59//! status lives in the trailers. A streaming client built on 0.1.1 could not tell a
60//! failed stream from a successfully empty one. `Response::into_parts` fixes that.
61//!
62//! ## Reconnect
63//!
64//! [`Client`] is a gRPC **channel**, not a handle to a socket: the tunnel opens on
65//! the first call and **reopens if it drops**, the way `tonic::transport::Channel`
66//! does. The call that discovers a dead tunnel reports the failure and the next one
67//! reconnects — the transport never silently replays a request the server may
68//! already have seen, because that is a retry policy decision and not its to make.
69//!
70//! [`Client::state`] and [`Client::state_changes`] surface where the channel is
71//! (gRPC's connectivity states, `WaitForStateChange` as a stream), so an app can
72//! say something useful instead of guessing from a failed call.
73//!
74//! There is no reconnect **backoff**: like tonic, a redial happens when a call asks
75//! for one, so the call rate bounds the dial rate. A client built with
76//! [`Client::over_transport`] cannot redial at all — the transport is consumed —
77//! and says so rather than pretending to be live.
78
79mod client;
80mod codec;
81mod metadata;
82mod state;
83mod status;
84mod url;
85
86pub use client::{CallOptions, Client, Streaming, UnaryResponse};
87pub use client::Connector;
88pub use state::ConnectivityState;
89pub use codec::{encode_message, Deframer};
90pub use metadata::{Metadata, MetadataValue};
91pub use status::{Code, Status};
92
93// Re-exported so callers can tune the tunnel — or supply their own — without
94// depending on h2ts-client directly. `TransportError` belongs here too: a caller
95// building a `Transport` has to name the error type its sink produces, so leaving it
96// out meant `Client::over_transport` could not be used without adding h2ts-client as
97// a second dependency. `H2Connection` is here for the same reason one step up:
98// [`Connector`] is a function returning one, so a *reconnecting* client could not be
99// built without naming the type.
100pub use h2ts_client::{ConnectOptions, H2Connection, Transport, TransportError};
101
102/// Open an h2ts tunnel over a byte transport: the connection, plus the driver future
103/// the caller must poll for anything to happen.
104///
105/// This is `h2ts_client::connect`, re-exported because a [`Connector`] has to produce
106/// an [`H2Connection`] and there was otherwise no way to make one from here. In a
107/// browser prefer [`connect`], which builds the whole reconnecting client.
108pub use h2ts_client::connect as open_tunnel;
109
110/// The WebSocket subprotocol an h2ts client offers.
111pub const H2TS_SUBPROTOCOL: &str = h2ts_client::DEFAULT_SUBPROTOCOL;
112
113#[cfg(target_arch = "wasm32")]
114mod web;
115#[cfg(target_arch = "wasm32")]
116pub use web::connect;
117
118#[cfg(feature = "prost")]
119mod typed;
120#[cfg(feature = "prost")]
121pub use typed::{TypedClient, TypedResponse};
122
123#[cfg(feature = "tonic")]
124pub mod tonic_service;
125#[cfg(feature = "tonic")]
126pub use tonic_service::{ResponseBody, TonicService};