Skip to main content

web_rpc/
lib.rs

1//! Bidirectional RPC for browsing contexts, web workers, and message channels.
2//!
3//! This crate allows you to define a service as a trait and annotate it with
4//! [`#[web_rpc::service]`](macro@service). The macro then produces a `*Client`, a `*Service`,
5//! a forwarding trait that you can implement on the server side, and a compile-time
6//! [description](describe::Service) of the trait from which
7//! [`js::endpoint!`](macro@js::endpoint) can render a typed Javascript endpoint.
8//!
9//! Routing is explicit. A value wrapped in [`Post`](wrap::Post) or [`Transfer`](wrap::Transfer)
10//! crosses the channel as a Javascript value through `postMessage`; anything else is encoded
11//! with [postcard](https://docs.rs/postcard) and must implement
12//! [`postcard_schema::Schema`]. There is special support for `Option<T>` and `Result<T, E>`
13//! so that Javascript values can be embedded within them, and this behaviour is recursive.
14//!
15//! # Quickstart
16//! ```rust
17//! #[web_rpc::service]
18//! pub trait Calculator {
19//!     fn add(&self, left: u32, right: u32) -> u32;
20//! }
21//! struct Calc;
22//! impl Calculator for Calc {
23//!     fn add(&self, left: u32, right: u32) -> u32 { left + right }
24//! }
25//! ```
26//! Wire up over a `MessageChannel`, [`Worker`](https://docs.rs/web-sys/latest/web_sys/struct.Worker.html),
27//! or any [`MessagePort`](https://docs.rs/web-sys/latest/web_sys/struct.MessagePort.html).
28//! Each call to [`Interface::new`] is async because temporary listeners need to detect when
29//! both ends are ready.
30//! ```rust,no_run
31//! # #[web_rpc::service]
32//! # pub trait Calculator { fn add(&self, l: u32, r: u32) -> u32; }
33//! # struct Calc;
34//! # impl Calculator for Calc { fn add(&self, l: u32, r: u32) -> u32 { l + r } }
35//! # async fn run() {
36//! let channel = web_sys::MessageChannel::new().unwrap();
37//! channel.port1().start();
38//! channel.port2().start();
39//! let (server_iface, client_iface) = futures_util::future::join(
40//!     web_rpc::Interface::new(channel.port1()),
41//!     web_rpc::Interface::new(channel.port2()),
42//! ).await;
43//!
44//! let server = web_rpc::Builder::new(server_iface)
45//!     .with_service::<CalculatorService<_>>(Calc)
46//!     .build();
47//! wasm_bindgen_futures::spawn_local(server);
48//!
49//! let client = web_rpc::Builder::new(client_iface)
50//!     .with_client::<CalculatorClient>()
51//!     .build();
52//! assert_eq!(client.add(41, 1).await, 42);
53//! # }
54//! ```
55//!
56//! # Transports are borrowed, never owned
57//! web-rpc uses the transport it is handed and never manages its lifecycle. Dropping a
58//! [`Port`](port::Port), an [`Interface`] or a client does not terminate a
59//! [`Worker`](web_sys::Worker): whoever created the worker terminates it. Likewise a
60//! [`MessagePort`](web_sys::MessagePort) is **not** started for you, on either the Rust or the
61//! Javascript side. Call [`start`](web_sys::MessagePort::start) on it before handing it over,
62//! as in the example above; an unstarted port delivers nothing to the listener that
63//! [`Interface::new`] installs, so the symptom is a handshake that spins forever rather than an
64//! error.
65//!
66//! # Routing
67//! ```rust
68//! use web_rpc::wrap::{Post, Transfer};
69//!
70//! #[web_rpc::service]
71//! pub trait Routing {
72//!     // Plain types implementing `Serialize` and `Schema` go through postcard.
73//!     fn add(&self, l: u32, r: u32) -> u32;
74//!     // `Post<T>` crosses as a Javascript value, copied by structured clone.
75//!     fn echo(&self, s: Post<js_sys::JsString>) -> Post<js_sys::JsString>;
76//!     // `Transfer<T>` crosses as a Javascript value and is moved, not copied.
77//!     fn upload(&self, buffer: Transfer<js_sys::ArrayBuffer>) -> u32;
78//!     // `Option`/`Result` recurse: each variant routes independently.
79//!     fn lookup(&self, k: u32) -> Result<Option<Post<js_sys::JsString>>, String>;
80//!     // `&str` / `&[u8]` deserialize zero-copy on the server.
81//!     fn count(&self, data: &[u8]) -> u32;
82//! }
83//! ```
84//! A bare Javascript type in a signature is a compile error, because it implements neither
85//! [`serde::Serialize`] nor [`postcard_schema::Schema`]. Note that a typed array is not a
86//! transferable object: send `Transfer<ArrayBuffer>` and rebuild the view on the other side.
87//!
88//! Every type in a signature must implement [`postcard_schema::Schema`], which for your own
89//! payload types means `#[derive(Schema)]` alongside the serde derives. The trait description,
90//! and therefore the generated Javascript, is built from it. postcard-schema implements `Schema`
91//! for neither `usize` nor `isize`, since serde widens both to 64 bits, so use a fixed-width
92//! integer in a signature; and a foreign type with no upstream `Schema` impl needs a local
93//! mirror type.
94//!
95//! # Async, notifications, streaming
96//! ```rust
97//! use futures_core::Stream;
98//!
99//! #[web_rpc::service]
100//! pub trait Misc {
101//!     // `async` here makes the server impl async; the client side is also async because we return a u32.
102//!     async fn slow(&self, ms: u32) -> u32;
103//!     // No return type means the method is a notification.
104//!     fn fire(&self, msg: String);
105//!     // `impl Stream<Item = T>` makes the method a streaming RPC.
106//!     fn items(&self, n: u32) -> impl Stream<Item = u32>;
107//! }
108//! ```
109//! On the client side, RPC methods that have a return type are async and yield a
110//! [`client::RequestFuture<T>`] which you await for the response. Methods without a return type
111//! are sync and act as fire-and-forget notifications. This is independent of whether the trait
112//! method itself is marked `async`, which only affects the server implementation. Dropping the
113//! `RequestFuture` cancels the request, so notifications cannot be cancelled.
114//!
115//! Streaming methods return a [`client::StreamReceiver<T>`] that yields each item the server
116//! produces. Dropping the receiver aborts the stream on the server, while
117//! [`close`](client::StreamReceiver::close) lets buffered items finish arriving instead.
118//! Streaming methods can also be `async` and the items they yield can be wrapper types like
119//! `Result<Post<JsT>, E>`.
120//!
121//! # Conditional methods
122//! Methods can be gated with `#[cfg(...)]` or `#[cfg_attr(...)]`. The macro propagates these
123//! attributes to every generated artifact for that method, so rustc strips them in lockstep.
124//! ```rust
125//! #[web_rpc::service]
126//! pub trait Conditional {
127//!     fn always_on(&self, x: u32) -> u32;
128//!     #[cfg(feature = "admin")]
129//!     fn extra(&self, s: &str) -> String;
130//! }
131//! ```
132//! Postcard encodes enum variants by their positional discriminant, so the set of methods
133//! that survive cfg evaluation must match on both ends of a channel. If one side has a gated
134//! method enabled and the other does not, the wire format will silently desync.
135//!
136//! # Bi-directional
137//! Both sides of a channel can be set up to act as both client and server at the same time. To
138//! do this, stack [`with_service`](Builder::with_service) and
139//! [`with_client`](Builder::with_client) on the same [`Builder`] before calling `build()`, which
140//! then returns a `(C, Server)` tuple instead of one or the other.
141//! ```rust,no_run
142//! # #[web_rpc::service]
143//! # pub trait Calculator { fn add(&self, l: u32, r: u32) -> u32; }
144//! # struct Calc;
145//! # impl Calculator for Calc { fn add(&self, l: u32, r: u32) -> u32 { l + r } }
146//! # async fn run() {
147//! # let channel = web_sys::MessageChannel::new().unwrap();
148//! # let (iface, _) = futures_util::future::join(
149//! #     web_rpc::Interface::new(channel.port1()),
150//! #     web_rpc::Interface::new(channel.port2()),
151//! # ).await;
152//! let (client, server) = web_rpc::Builder::new(iface)
153//!     .with_service::<CalculatorService<_>>(Calc)
154//!     .with_client::<CalculatorClient>()
155//!     .build();
156//! # }
157//! ```
158//!
159//! # Javascript endpoints
160//! [`js::endpoint!`](macro@js::endpoint) renders a Javascript class and a `.d.ts` for the other
161//! end of a connection, from the same traits, into two custom sections of the wasm binary. See
162//! the [`js`] module.
163
164use std::{
165    cell::RefCell,
166    marker::PhantomData,
167    pin::Pin,
168    rc::Rc,
169    task::{Context, Poll},
170};
171
172use futures_channel::mpsc;
173use futures_core::{future::LocalBoxFuture, Future};
174use futures_util::{future::Shared, FutureExt, StreamExt};
175use gloo_events::EventListener;
176use js_sys::{Array, ArrayBuffer, Uint8Array};
177use serde::{de::DeserializeOwned, Deserialize, Serialize};
178use wasm_bindgen::JsCast;
179
180#[doc(hidden)]
181pub use futures_channel;
182#[doc(hidden)]
183pub use futures_core;
184#[doc(hidden)]
185pub use futures_util;
186#[doc(hidden)]
187pub use gloo_events;
188#[doc(hidden)]
189pub use js_sys;
190#[doc(hidden)]
191pub use postcard;
192#[doc(hidden)]
193pub use postcard_schema;
194#[doc(hidden)]
195pub use serde;
196#[doc(hidden)]
197pub use wasm_bindgen;
198#[doc(hidden)]
199pub use web_sys;
200
201pub use web_rpc_macro::service;
202
203pub mod client;
204#[doc(hidden)]
205pub mod codec;
206pub mod describe;
207pub mod interface;
208pub mod js;
209pub mod port;
210#[doc(hidden)]
211pub mod service;
212pub mod wrap;
213
214pub use interface::Interface;
215use port::Port;
216
217/// The first element of every message. The sequence number is allocated by whoever sends
218/// the request, and identifies a message only within the direction it travels.
219#[doc(hidden)]
220#[derive(Serialize, Deserialize)]
221pub enum MessageHeader {
222    Request(u32),
223    Abort(u32),
224    Response(u32),
225    StreamItem(u32),
226    StreamEnd(u32),
227}
228
229/// The future that turns inbound messages into responses, stream items and requests. It is
230/// shared by every client and server on one interface and driven by whichever of them is
231/// polled, and it completes only when the listener that feeds it is dropped.
232#[doc(hidden)]
233pub type Dispatcher = Shared<LocalBoxFuture<'static, ()>>;
234
235fn to_buffer(bytes: &[u8]) -> ArrayBuffer {
236    Uint8Array::from(bytes).buffer()
237}
238
239/// Take the `ArrayBuffer` at the front of a message and copy it out.
240#[doc(hidden)]
241pub fn take_bytes(message: &Array) -> Vec<u8> {
242    let buffer = message
243        .shift()
244        .dyn_into::<ArrayBuffer>()
245        .expect("web_rpc: a message must start with an ArrayBuffer");
246    Uint8Array::new(&buffer).to_vec()
247}
248
249/// Post a message that is only a header.
250#[doc(hidden)]
251pub fn post_header(port: &Port, header: MessageHeader) {
252    let header = to_buffer(&postcard::to_allocvec(&header).unwrap());
253    let message = Array::of1(&header);
254    port.post_message(&message, &message).unwrap();
255}
256
257/// Post `[header, payload, ...post_args]`, transferring the buffers and `transfer_args`.
258#[doc(hidden)]
259pub fn post_message(
260    port: &Port,
261    header: MessageHeader,
262    payload: &impl Serialize,
263    post_args: &Array,
264    transfer_args: &Array,
265) {
266    let header = to_buffer(&postcard::to_allocvec(&header).unwrap());
267    let payload = to_buffer(&postcard::to_allocvec(payload).unwrap());
268    post_args.unshift(&payload);
269    post_args.unshift(&header);
270    transfer_args.unshift(&payload);
271    transfer_args.unshift(&header);
272    port.post_message(post_args, transfer_args).unwrap();
273}
274
275/// This struct allows one to configure the RPC interface prior to creating it.
276/// To get an instance of this struct, call [`Builder<C, S>::new`] with
277/// an [`Interface`].
278pub struct Builder<C, S> {
279    client: PhantomData<C>,
280    service: S,
281    interface: Interface,
282}
283
284impl Builder<(), ()> {
285    /// Create a new builder from an [`Interface`]
286    pub fn new(interface: Interface) -> Self {
287        Self {
288            interface,
289            client: PhantomData,
290            service: (),
291        }
292    }
293}
294
295impl<C> Builder<C, ()> {
296    /// Configure the RPC interface with a service that implements methods
297    /// that can be called from the other side of the channel. To use this method,
298    /// you need to specify the type `S` which is the service type generated by the
299    /// attribute macro [`macro@service`]. The implementation parameter is then an
300    /// instance of something that implements the trait to which you applied the
301    /// [`macro@service`] macro. For example, if you have a trait `Calculator` to
302    /// which you have applied [`macro@service`], you would use this method as follows:
303    /// ```rust,no_run
304    /// # #[web_rpc::service]
305    /// # pub trait Calculator {
306    /// #     fn add(&self, left: u32, right: u32) -> u32;
307    /// # }
308    /// # struct CalculatorServiceImpl;
309    /// # impl Calculator for CalculatorServiceImpl {
310    /// #     fn add(&self, left: u32, right: u32) -> u32 { left + right }
311    /// # }
312    /// # fn example(some_interface: web_rpc::Interface) {
313    /// let server = web_rpc::Builder::new(some_interface)
314    ///     .with_service::<CalculatorService<_>>(CalculatorServiceImpl)
315    ///     .build();
316    /// # }
317    /// ```
318    pub fn with_service<S: service::Service>(self, implementation: impl Into<S>) -> Builder<C, S> {
319        Builder {
320            interface: self.interface,
321            client: self.client,
322            service: implementation.into(),
323        }
324    }
325}
326
327impl<S> Builder<(), S> {
328    /// Configure the RPC interface with a client that allows you to execute RPCs on the
329    /// server. The builder instantiates the client for you, you just
330    /// need to provide the type which is generated via the [`macro@service`] attribute
331    /// macro. For example, if you had a trait `Calculator` to which you applied the
332    /// [`macro@service`] attribute macro, the macro would have generated a `CalculatorClient`
333    /// struct which you can use as the `C` in this function.
334    pub fn with_client<C: client::Client>(self) -> Builder<C, S> {
335        Builder {
336            interface: self.interface,
337            client: PhantomData,
338            service: self.service,
339        }
340    }
341}
342
343/// `Server` is the server that is returned from the [`Builder::build`] method given
344/// you configured the RPC interface with a service. Note that `Server` implements future and needs
345/// to be polled in order to execute and respond to inbound RPC requests.
346#[must_use = "Server must be polled in order for RPC requests to be executed"]
347pub struct Server {
348    _listener: Rc<EventListener>,
349    task: LocalBoxFuture<'static, ()>,
350}
351
352impl Future for Server {
353    type Output = ();
354
355    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
356        self.task.poll_unpin(cx)
357    }
358}
359
360/// The client half of an interface with no client: it receives nothing and is never handed
361/// out.
362struct NoClient;
363
364impl client::Client for NoClient {
365    type Response = ();
366}
367
368impl From<client::State<()>> for NoClient {
369    fn from(_: client::State<()>) -> Self {
370        NoClient
371    }
372}
373
374/// The service half of an interface with no service: its server is dropped unpolled.
375struct NoService;
376
377impl service::Service for NoService {
378    type Response = ();
379
380    async fn execute(
381        &self,
382        _: u32,
383        _: futures_channel::oneshot::Receiver<()>,
384        _: Vec<u8>,
385        _: Array,
386        _: mpsc::UnboundedSender<service::StreamMessage<()>>,
387    ) -> (u32, service::ExecuteResult<()>) {
388        unreachable!("web_rpc: a request reached an interface with no service")
389    }
390}
391
392/// Build both halves of an interface. Whichever half the caller did not ask for is built
393/// from its `No*` stand-in and dropped.
394fn assemble<C, S>(interface: Interface, service: S) -> (C, Server)
395where
396    C: client::Client + From<client::State<C::Response>> + 'static,
397    C::Response: DeserializeOwned,
398    S: service::Service + 'static,
399    S::Response: Serialize,
400{
401    let Interface {
402        port,
403        listener,
404        mut messages_rx,
405    } = interface;
406    let callbacks: Rc<RefCell<client::CallbackMap<C::Response>>> = Default::default();
407    let stream_callbacks: Rc<RefCell<client::StreamCallbackMap<C::Response>>> = Default::default();
408    let (requests_tx, requests_rx) = mpsc::unbounded();
409    let (aborts_tx, aborts_rx) = mpsc::unbounded();
410    let dispatcher: Dispatcher = {
411        let callbacks = callbacks.clone();
412        let stream_callbacks = stream_callbacks.clone();
413        async move {
414            while let Some(message) = messages_rx.next().await {
415                let header: MessageHeader = postcard::from_bytes(&take_bytes(&message)).unwrap();
416                match header {
417                    MessageHeader::Request(sequence) => {
418                        let payload = take_bytes(&message);
419                        requests_tx
420                            .unbounded_send((sequence, payload, message))
421                            .expect("web_rpc: a request arrived but the server has been dropped");
422                    }
423                    MessageHeader::Abort(sequence) => {
424                        let _ = aborts_tx.unbounded_send(sequence);
425                    }
426                    MessageHeader::Response(sequence) => {
427                        let response = postcard::from_bytes(&take_bytes(&message)).unwrap();
428                        if let Some(callback) = callbacks.borrow_mut().remove(&sequence) {
429                            let _ = callback.send((response, message));
430                        }
431                    }
432                    MessageHeader::StreamItem(sequence) => {
433                        let item = postcard::from_bytes(&take_bytes(&message)).unwrap();
434                        if let Some(items) = stream_callbacks.borrow().get(&sequence) {
435                            let _ = items.unbounded_send((item, message));
436                        }
437                    }
438                    MessageHeader::StreamEnd(sequence) => {
439                        stream_callbacks.borrow_mut().remove(&sequence);
440                    }
441                }
442            }
443        }
444        .boxed_local()
445        .shared()
446    };
447    let listener = Rc::new(listener);
448    let client = C::from(client::State {
449        callbacks,
450        stream_callbacks,
451        port: port.clone(),
452        listener: listener.clone(),
453        dispatcher: dispatcher.clone(),
454        sequence: Default::default(),
455    });
456    let server = Server {
457        _listener: listener,
458        task: service::task::<S>(service, port, dispatcher, requests_rx, aborts_rx).boxed_local(),
459    };
460    (client, server)
461}
462
463impl<C> Builder<C, ()>
464where
465    C: client::Client + From<client::State<C::Response>> + 'static,
466    C::Response: DeserializeOwned,
467{
468    /// Build function for client-only RPC interfaces.
469    pub fn build(self) -> C {
470        assemble::<C, NoService>(self.interface, NoService).0
471    }
472}
473
474impl<S> Builder<(), S>
475where
476    S: service::Service + 'static,
477    S::Response: Serialize,
478{
479    /// Build function for server-only RPC interfaces.
480    pub fn build(self) -> Server {
481        assemble::<NoClient, S>(self.interface, self.service).1
482    }
483}
484
485impl<C, S> Builder<C, S>
486where
487    C: client::Client + From<client::State<C::Response>> + 'static,
488    C::Response: DeserializeOwned,
489    S: service::Service + 'static,
490    S::Response: Serialize,
491{
492    /// Build function for client-server RPC interfaces.
493    pub fn build(self) -> (C, Server) {
494        assemble::<C, S>(self.interface, self.service)
495    }
496}