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, and a forwarding trait that you can implement on the server side.

Routing is inferred from each type: anything implementing AsRef<JsValue> is posted through postMessage directly and everything that is serializable is first encoded via bincode. There is special support for Option<T> and Result<T, E> to allow Javascript types to be embedded within these types. 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 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();
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);

§Routing

#[web_rpc::service]
pub trait Routing {
    // Plain types that implement Serialize go through bincode.
    fn add(&self, l: u32, r: u32) -> u32;
    // Anything `AsRef<JsValue>` is posted through the JS array.
    fn echo(&self, s: js_sys::JsString) -> js_sys::JsString;
    // `Option`/`Result` recurse: Ok(Some(_)) is posted, Ok(None) is one byte,
    // Err carries a bincoded `String`.
    fn lookup(&self, k: u32) -> Result<Option<js_sys::JsString>, String>;
    // `&str` / `&[u8]` deserialize zero-copy on the server.
    fn count(&self, data: &[u8]) -> usize;
    // References to JS types are accepted too and are decoded via JsCast::dyn_ref.
    fn len(&self, s: &js_sys::JsString) -> u32;
}

§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<JsT, E>.

§Transfer

Anything that should be transferred to the other side rather than copied with the structured clone algorithm can be specified inside a #[transfer(...)] attribute as a comma-separated list. The simplest case is to list the parameter that holds the transferable value, but if that value is wrapped or derived from a parameter, you can use a parameter-name expression (name => expr, evaluated with name in scope), a closure with a refutable pattern (name => |pat| body), or a match-block (name => match { arm, ... }). The same forms also work for the return value via return.

#[web_rpc::service]
pub trait Transfer {
    // Bare param + derived expression + return closure.
    #[transfer(
        canvas,
        data => data.buffer(),
        return => |Ok(buf)| buf.buffer(),
    )]
    fn render(
        &self,
        canvas: web_sys::OffscreenCanvas,
        data: js_sys::Uint8Array,
    ) -> Result<js_sys::Uint8Array, String>;

    // Match-block: useful when several variants need transferring.
    #[transfer(return => match { Some(buf) => buf.buffer(), })]
    fn maybe(&self) -> Option<js_sys::Uint8Array>;
}

§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();

Re-exports§

pub use interface::Interface;

Modules§

client
interface
port

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 applied to traits that need to be turned into RPCs. The macro will consume the trait and output three items in its place. For example, a trait Calculator will be replaced with two structs CalculatorClient and CalculatorService and a new trait by the same name. All methods must include &self as their first parameter.