Expand description
Bidirectional RPC for browsing contexts, web workers, and message channels.
This crate allows you to define a service as a trait and annotate it with
#[web_rpc::service]. The macro then produces a *Client, a *Service,
a forwarding trait that you can implement on the server side, and a compile-time
description of the trait from which
js::endpoint! can render a typed Javascript endpoint.
Routing is explicit. A value wrapped in Post or Transfer
crosses the channel as a Javascript value through postMessage; anything else is encoded
with postcard and must implement
postcard_schema::Schema. There is special support for Option<T> and Result<T, E>
so that Javascript values can be embedded within them, and this behaviour is recursive.
§Quickstart
#[web_rpc::service]
pub trait Calculator {
fn add(&self, left: u32, right: u32) -> u32;
}
struct Calc;
impl Calculator for Calc {
fn add(&self, left: u32, right: u32) -> u32 { left + right }
}Wire up over a MessageChannel, Worker,
or any MessagePort.
Each call to Interface::new is async because temporary listeners need to detect when
both ends are ready.
let channel = web_sys::MessageChannel::new().unwrap();
channel.port1().start();
channel.port2().start();
let (server_iface, client_iface) = futures_util::future::join(
web_rpc::Interface::new(channel.port1()),
web_rpc::Interface::new(channel.port2()),
).await;
let server = web_rpc::Builder::new(server_iface)
.with_service::<CalculatorService<_>>(Calc)
.build();
wasm_bindgen_futures::spawn_local(server);
let client = web_rpc::Builder::new(client_iface)
.with_client::<CalculatorClient>()
.build();
assert_eq!(client.add(41, 1).await, 42);§Transports are borrowed, never owned
web-rpc uses the transport it is handed and never manages its lifecycle. Dropping a
Port, an Interface or a client does not terminate a
Worker: whoever created the worker terminates it. Likewise a
MessagePort is not started for you, on either the Rust or the
Javascript side. Call start on it before handing it over,
as in the example above; an unstarted port delivers nothing to the listener that
Interface::new installs, so the symptom is a handshake that spins forever rather than an
error.
§Routing
use web_rpc::wrap::{Post, Transfer};
#[web_rpc::service]
pub trait Routing {
// Plain types implementing `Serialize` and `Schema` go through postcard.
fn add(&self, l: u32, r: u32) -> u32;
// `Post<T>` crosses as a Javascript value, copied by structured clone.
fn echo(&self, s: Post<js_sys::JsString>) -> Post<js_sys::JsString>;
// `Transfer<T>` crosses as a Javascript value and is moved, not copied.
fn upload(&self, buffer: Transfer<js_sys::ArrayBuffer>) -> u32;
// `Option`/`Result` recurse: each variant routes independently.
fn lookup(&self, k: u32) -> Result<Option<Post<js_sys::JsString>>, String>;
// `&str` / `&[u8]` deserialize zero-copy on the server.
fn count(&self, data: &[u8]) -> u32;
}A bare Javascript type in a signature is a compile error, because it implements neither
serde::Serialize nor postcard_schema::Schema. Note that a typed array is not a
transferable object: send Transfer<ArrayBuffer> and rebuild the view on the other side.
Every type in a signature must implement postcard_schema::Schema, which for your own
payload types means #[derive(Schema)] alongside the serde derives. The trait description,
and therefore the generated Javascript, is built from it. postcard-schema implements Schema
for neither usize nor isize, since serde widens both to 64 bits, so use a fixed-width
integer in a signature; and a foreign type with no upstream Schema impl needs a local
mirror type.
§Async, notifications, streaming
use futures_core::Stream;
#[web_rpc::service]
pub trait Misc {
// `async` here makes the server impl async; the client side is also async because we return a u32.
async fn slow(&self, ms: u32) -> u32;
// No return type means the method is a notification.
fn fire(&self, msg: String);
// `impl Stream<Item = T>` makes the method a streaming RPC.
fn items(&self, n: u32) -> impl Stream<Item = u32>;
}On the client side, RPC methods that have a return type are async and yield a
client::RequestFuture<T> which you await for the response. Methods without a return type
are sync and act as fire-and-forget notifications. This is independent of whether the trait
method itself is marked async, which only affects the server implementation. Dropping the
RequestFuture cancels the request, so notifications cannot be cancelled.
Streaming methods return a client::StreamReceiver<T> that yields each item the server
produces. Dropping the receiver aborts the stream on the server, while
close lets buffered items finish arriving instead.
Streaming methods can also be async and the items they yield can be wrapper types like
Result<Post<JsT>, E>.
§Conditional methods
Methods can be gated with #[cfg(...)] or #[cfg_attr(...)]. The macro propagates these
attributes to every generated artifact for that method, so rustc strips them in lockstep.
#[web_rpc::service]
pub trait Conditional {
fn always_on(&self, x: u32) -> u32;
#[cfg(feature = "admin")]
fn extra(&self, s: &str) -> String;
}Postcard encodes enum variants by their positional discriminant, so the set of methods that survive cfg evaluation must match on both ends of a channel. If one side has a gated method enabled and the other does not, the wire format will silently desync.
§Bi-directional
Both sides of a channel can be set up to act as both client and server at the same time. To
do this, stack with_service and
with_client on the same Builder before calling build(), which
then returns a (C, Server) tuple instead of one or the other.
let (client, server) = web_rpc::Builder::new(iface)
.with_service::<CalculatorService<_>>(Calc)
.with_client::<CalculatorClient>()
.build();§Javascript endpoints
js::endpoint! renders a Javascript class and a .d.ts for the other
end of a connection, from the same traits, into two custom sections of the wasm binary. See
the js module.
Re-exports§
pub use interface::Interface;
Modules§
- client
- describe
- The compile-time description of a service trait.
- interface
- js
- Javascript endpoints rendered from service traits at compile time.
- port
- wrap
- Explicit routing wrappers.
Structs§
- Builder
- This struct allows one to configure the RPC interface prior to creating it.
To get an instance of this struct, call
Builder<C, S>::newwith anInterface. - Server
Serveris the server that is returned from theBuilder::buildmethod given you configured the RPC interface with a service. Note thatServerimplements future and needs to be polled in order to execute and respond to inbound RPC requests.
Attribute Macros§
- service
- This attribute macro should be applied to traits that need to be turned into RPCs. The macro
consumes the trait and outputs four items in its place. For a trait
Calculatorthose are the structsCalculatorClientandCalculatorService, a new trait by the same name, and aCALCULATOR_DESCRIPTIONconst describing the trait forweb_rpc::js::endpoint!. All methods must include&selfas their first parameter.