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}