"LiveView" frameworks like Phoenix LiveView, LiveViewJS, and Dioxus LiveView are amazing. But why limit ourselves to LiveView? Why not LiveEverything?
WSDOM is a Rust → JavaScript Remote Method Invocation or Distributed Objects system. It lets Rust code hold JavaScript objects and call methods/functions over the network.
WSDOM can be used to add network-dependent functionalities to webpages without writing JS code or making API endpoints. It can also be integrated into "LiveView"-style Rust web frameworks to expose access to the full Web API.
Here is an example using WSDOM to put <div>Hello World!</div> on a webpage.
// this Rust code runs on your web server
fn hello(browser: wsdom::Browser) {
let document = wsdom::dom::document(&browser) // get hold of a Document object
let body = document.get_body(); // get the <body /> of that document object
let elem = document.create_element(&"div", &wsdom::undefined()); // create a <div />
elem.set_inner_text(&"Hello World!"); // set the text
body.append_child(&elem); // add the <div /> to the <body />
}// this JavaScript code runs on the browser
import { WSDOM } from "./servant.js";
import { WSDOMTransport } from "./transport.js";
const connection = new WSDOMTransport(WSDOM, [{}], {
transport: { kind: "websocket", url: "ws://my-website.domain:4000/" },
});
await connection.start();Our full "Hello World!" code is available here.
- WSDOM generates strongly-typed Rust stubs for JS classes/functions/methods based on
.d.tsTypeScript definitions. - Calling JS code with WSDOM is roundtrip-free. This Rust code
does not block on the network at all; it will finish in microseconds.
let mut val: JsNumber = browser.new_value(&1.0); for _ in 0..100 { val = wsdom::js::Math::cos(&browser, &val); // Math.cos on the JS side }
- Roundtrip-free calling is possible because WSDOM keeps values on the JS side, sending them back to Rust only when explicitly requested.
To get the value computed by the loop above, one would do
the
let val_retrieved: f64 = val.retrieve_float().await; println!("the value of (cos^[100])(1.0) computed in JavaScript is {val_retrieved}");
.awaitwill take one network roundtrip.
- Roundtrip-free calling is possible because WSDOM keeps values on the JS side, sending them back to Rust only when explicitly requested.
To get the value computed by the loop above, one would do
- Due to the roundtrip-free design, WSDOM fundamentally cannot handle JS exceptions.
- If one of the
Math.coscalls in our loop above throws, the Rust loop will still complete all 100 iterations without panic or any sort of warning (see How It Works for why). As you might expect, this means code using WSDOM are very painful to debug.
- If one of the
- WSDOM is one-way. Rust code can call JS code but not the other way around.
- To make event handling possible, we have Futures-based interactivity;
we connect JS callbacks to streams that can be awaited on the Rust side.
async fn example(browser: Browser, button: &HTMLElement) { let (stream, callback) = wsdom::callback::new_callback::<MouseEvent>(&browser); button.add_event_listener(&"click", &callback, &wsdom::undefined()); let _click_event: Option<MouseEvent> = stream.next().await; // wait for the Stream to yield println!("button was clicked on the browser!"); }
- To make event handling possible, we have Futures-based interactivity;
we connect JS callbacks to streams that can be awaited on the Rust side.
- WSDOM is transport-agnostic, framework-agnostic, and executor-agnostic. That said, we provide an integration library for easily getting started with WSDOM on Axum web framework (which uses the Tokio executor) with WebSocket.
Hosted examples are available. When viewing them, I recommend opening your browser's network devtool to see the WebSocket traffic.
WSDOM serves a similar role as web-sys (and a bit of js-sys too), but instead of running your Rust in WebAssembly in the same browser, we let you run your Rust code away across a WebSocket connection.
WSDOM's translation of JS API to Rust is different from web-sys.
We translate from TypeScript declarations, rather than directly from WebIDLs.
The network gap also means our optional types take the form of JsNullable<_> (compared to the Option<_> of web-sys).
WSDOM and jsdom are similar in that we both expose the web browser's API outside a web browser. jsdom does so by implementing the API themselves. WSDOM does so by forwarding calls to a real web browser running across a WebSocket connection.
WSDOM's private _w protocol object can use a complete, consistent
property-mangled name scheme. Existing callers retain canonical protocol
spelling; configurable source emitters must use the safe resolver helpers.
See PROTOCOL_HOST_METHOD_NAMES.md.
WSDOMTransport is a standalone TypeScript module. It constructs any generated,
sender-first WSDOM class and exposes that client as connection.client. Choose a
transport explicitly: websocket accepts a URL and optional WebSocket protocols;
long-poll repeatedly POSTs to one endpoint and defaults to a 1-second interval.
The long-poll request and response bodies are both JSON arrays of ordered protocol
strings. Session identity and authentication belong in the endpoint URL, cookies, or
requestInit headers/credentials. Polling and WebSocket failures are reported through
onError and retry with bounded exponential backoff.
Protocol wrappers are async duplex middleware. Their outbound hooks must forward
application frames with context.sendOutbound; inbound hooks must forward received
frames with context.sendInbound. A wrapper may emit its own control frames in either
direction, keep request/reply state, and call context.interact(request) when the host
supplies an async interact handler.
The How It Works document describes how WSDOM works in more details.
The crate is on crates.io and the documentation is on docs.rs.
Use WSDOM at your own risk. It is alpha-quality at best.
The Rust code produced by our .d.ts loader might change between WSDOM versions.