Skip to main content

Crate web_rpc

Crate web_rpc 

Source
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>::new with an Interface.
Server
Server is the server that is returned from the Builder::build method given you configured the RPC interface with a service. Note that Server implements 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 Calculator those are the structs CalculatorClient and CalculatorService, a new trait by the same name, and a CALCULATOR_DESCRIPTION const describing the trait for web_rpc::js::endpoint!. All methods must include &self as their first parameter.