This documentation is part of the "Projects with Books" initiative at zenOSmosis.
The source code for this project is available on GitHub.
WASM RPC Client
Relevant source files
- Cargo.lock
- extensions/muxio-rpc-service/Cargo.toml
- extensions/muxio-tokio-rpc-client/src/lib.rs
- extensions/muxio-wasm-rpc-client/Cargo.toml
- extensions/muxio-wasm-rpc-client/src/lib.rs
- extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs
- extensions/muxio-wasm-rpc-client/src/static_lib/static_client.rs
The WASM RPC Client provides a WebAssembly-compatible implementation of the RPC transport layer for browser environments. It bridges Rust code compiled to WASM with JavaScript's WebSocket API, enabling bidirectional RPC communication between WASM clients and native servers.
This page focuses on the client-side WASM implementation. For native Tokio-based clients, see Tokio RPC Client. For server-side implementations, see Tokio RPC Server. For the RPC abstraction layer, see RPC Framework.
Sources: extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:1-182 extensions/muxio-wasm-rpc-client/Cargo.toml:1-30
Architecture Overview
The WASM RPC client operates as a bridge between Rust WASM code and JavaScript's WebSocket API. Unlike the Tokio client which manages its own WebSocket connection, the WASM client relies on JavaScript glue code to handle WebSocket events and delegates to Rust for RPC protocol processing.
graph TB
subgraph "Browser JavaScript"
WS["WebSocket API"]
GLUE["JavaScript Glue Code\nmuxioWriteBytes()"]
APP["Web Application"]
end
subgraph "WASM Module (Rust)"
STATIC["MUXIO_STATIC_RPC_CLIENT_REF\nthread_local!"]
CLIENT["RpcWasmClient"]
DISPATCHER["RpcDispatcher"]
ENDPOINT["RpcServiceEndpoint"]
CALLER["RpcServiceCallerInterface"]
end
subgraph "Core Layer"
MUXIO["muxio core\nBinary Framing"]
end
APP -->|create WebSocket| WS
WS -->|onopen| GLUE
WS -->|onmessage bytes| GLUE
WS -->|onerror/onclose| GLUE
GLUE -->|handle_connect| STATIC
GLUE -->|read_bytes bytes| STATIC
GLUE -->|handle_disconnect| STATIC
STATIC -.->|Arc reference| CLIENT
CLIENT -->|uses| DISPATCHER
CLIENT -->|uses| ENDPOINT
CLIENT -.->|implements| CALLER
CLIENT -->|emit_callback bytes| GLUE
GLUE -->|send bytes| WS
DISPATCHER --> MUXIO
ENDPOINT --> MUXIO
The architecture consists of three layers:
- JavaScript Layer : Manages WebSocket lifecycle and forwards events to WASM
- WASM Bridge Layer :
RpcWasmClientand static client helpers - Core RPC Layer :
RpcDispatcherfor multiplexing andRpcServiceEndpointfor handling incoming calls
Sources: extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:16-35 extensions/muxio-wasm-rpc-client/src/static_lib/static_client.rs:9-36
RpcWasmClient Structure
The RpcWasmClient struct manages bidirectional RPC communication in WASM environments. It combines client-side call capabilities with server-side request handling.
classDiagram
class RpcWasmClient {
-Arc~Mutex~RpcDispatcher~~ dispatcher
-Arc~RpcServiceEndpoint~()~~ endpoint
-Arc~dyn Fn(Vec~u8~)~ emit_callback
-RpcTransportStateChangeHandler state_change_handler
-Arc~AtomicBool~ is_connected
+new(emit_callback) RpcWasmClient
+handle_connect() async
+read_bytes(bytes) async
+handle_disconnect() async
+is_connected() bool
+get_endpoint() Arc~RpcServiceEndpoint~()~~
}
class RpcServiceCallerInterface {<<trait>>\n+get_dispatcher() Arc~Mutex~RpcDispatcher~~\n+get_emit_fn() Arc~dyn Fn(Vec~u8~)~\n+is_connected() bool\n+set_state_change_handler(handler) async}
class RpcDispatcher {
+read_bytes(bytes) Result
+respond(response, chunk_size, callback) Result
+is_rpc_request_finalized(id) bool
+delete_rpc_request(id) Option
+fail_all_pending_requests(error)
}
class RpcServiceEndpoint {+get_prebuffered_handlers() Arc}
RpcWasmClient ..|> RpcServiceCallerInterface
RpcWasmClient --> RpcDispatcher
RpcWasmClient --> RpcServiceEndpoint
| Field | Type | Purpose |
|---|---|---|
dispatcher | Arc<Mutex<RpcDispatcher>> | Manages request/response correlation and stream multiplexing |
endpoint | Arc<RpcServiceEndpoint<()>> | Handles incoming RPC requests from the server |
emit_callback | Arc<dyn Fn(Vec<u8>)> | Callback to send bytes to JavaScript WebSocket |
state_change_handler | Arc<Mutex<Option<Box<dyn Fn(RpcTransportState)>>>> | Optional callback for connection state changes |
is_connected | Arc<AtomicBool> | Tracks connection status |
Sources: extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:16-24 extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:154-181
Connection Lifecycle
The WASM client relies on JavaScript to manage the WebSocket connection. Three lifecycle methods must be called from JavaScript glue code in response to WebSocket events:
stateDiagram-v2
[*] --> Disconnected : new()
Disconnected --> Connected : handle_connect()
Connected --> Processing : read_bytes(data)
Processing --> Connected
Connected --> Disconnected : handle_disconnect()
Disconnected --> [*]
note right of Connected
is_connected = true
state_change_handler(Connected)
end note
note right of Processing
1. read_bytes() into dispatcher
2. process_single_prebuffered_request()
3. respond() with results
end note
note right of Disconnected
is_connected = false
state_change_handler(Disconnected)
fail_all_pending_requests()
end note
handle_connect
Called when JavaScript's WebSocket onopen event fires. Updates connection state and notifies registered state change handlers.
Sources: extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:37-44
sequenceDiagram
participant JS as JavaScript
participant Client as RpcWasmClient
participant Dispatcher as RpcDispatcher
participant Endpoint as RpcServiceEndpoint
participant Handler as User Handler
JS->>Client: read_bytes(bytes)
Note over Client,Dispatcher: Stage 1: Synchronous Reading
Client->>Dispatcher: lock().read_bytes(bytes)
Dispatcher-->>Client: request_ids[]
Client->>Dispatcher: is_rpc_request_finalized(id)
Dispatcher-->>Client: true/false
Client->>Dispatcher: delete_rpc_request(id)
Dispatcher-->>Client: RpcRequest
Note over Client: Release dispatcher lock
Note over Client,Handler: Stage 2: Asynchronous Processing
loop For each request
Client->>Endpoint: process_single_prebuffered_request()
Endpoint->>Handler: invoke(request)
Handler-->>Endpoint: response
Endpoint-->>Client: RpcResponse
end
Note over Client,Dispatcher: Stage 3: Synchronous Sending
Client->>Dispatcher: lock().respond(response)
Dispatcher->>Client: emit_callback(chunk)
Client->>JS: muxioWriteBytes(chunk)
JS->>JS: websocket.send(chunk)
read_bytes
The core message processing method, called when JavaScript's WebSocket onmessage event fires. Implements a three-stage pipeline to avoid holding the dispatcher lock during expensive async operations:
Stage 1 : Acquires dispatcher lock, reads bytes into frame buffer, identifies finalized requests, and extracts them for processing. Lock is released immediately.
Stage 2 : Processes all requests concurrently without holding the dispatcher lock. User handlers execute async logic here.
Stage 3 : Re-acquires dispatcher lock briefly to serialize and send responses back through the emit callback.
This design prevents deadlocks and allows concurrent request processing while maintaining thread safety.
Sources: extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:46-121
handle_disconnect
Called when JavaScript's WebSocket onclose or onerror events fire. Updates connection state, notifies handlers, and fails all pending requests with a cancellation error.
Sources: extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:123-134
Static Client Pattern
For simplified JavaScript integration, the WASM client provides a static client pattern using thread-local storage. This eliminates the need to pass client instances through JavaScript and provides a global access point.
graph LR
subgraph "JavaScript"
INIT["init()"]
CALL["callSomeRpc()"]
end
subgraph "WASM Exports"
INIT_EXPORT["#[wasm_bindgen]\ninit_static_client()"]
RPC_EXPORT["#[wasm_bindgen]\nexported_rpc_function()"]
end
subgraph "Static Client Layer"
TLS["MUXIO_STATIC_RPC_CLIENT_REF\nthread_local!"]
WITH["with_static_client_async()"]
end
subgraph "Client Layer"
CLIENT["Arc<RpcWasmClient>"]
end
INIT --> INIT_EXPORT
INIT_EXPORT --> TLS
TLS -.stores.-> CLIENT
CALL --> RPC_EXPORT
RPC_EXPORT --> WITH
WITH --> TLS
TLS -.retrieves.-> CLIENT
WITH --> CLIENT
init_static_client
Initializes the thread-local static client reference. This function is idempotent—calling it multiple times has no effect after the first initialization. Typically called once during WASM module startup.
Sources: extensions/muxio-wasm-rpc-client/src/static_lib/static_client.rs:25-36
with_static_client_async
Primary method for interacting with the static client from exported WASM functions. Accepts a closure that receives the Arc<RpcWasmClient> and returns a future. Converts the result to a JavaScript Promise.
| Parameter | Type | Description |
|---|---|---|
f | FnOnce(Arc<RpcWasmClient>) -> Fut | Closure receiving client reference |
Fut | Future<Output = Result<T, String>> | Future returned by closure |
T | Into<JsValue> | Result type convertible to JavaScript value |
| Returns | Promise | JavaScript promise resolving to T or rejecting with error |
Sources: extensions/muxio-wasm-rpc-client/src/static_lib/static_client.rs:54-72
get_static_client
Returns the current static client if initialized, otherwise returns None. Useful for conditional logic or direct access without promise conversion.
Sources: extensions/muxio-wasm-rpc-client/src/static_lib/static_client.rs:79-81
JavaScript Integration
The WASM client requires JavaScript glue code to bridge WebSocket events to WASM function calls. Here's the typical integration pattern:
graph TB
subgraph "JavaScript WebSocket Events"
OPEN["ws.onopen"]
MESSAGE["ws.onmessage"]
ERROR["ws.onerror"]
CLOSE["ws.onclose"]
end
subgraph "WASM Bridge Functions"
WASM_CONNECT["wasm.handle_connect()"]
WASM_READ["wasm.read_bytes(event.data)"]
WASM_DISCONNECT["wasm.handle_disconnect()"]
end
subgraph "WASM Emit Callback"
EMIT["emit_callback(bytes)"]
WRITE["muxioWriteBytes(bytes)"]
end
OPEN --> WASM_CONNECT
MESSAGE --> WASM_READ
ERROR --> WASM_DISCONNECT
CLOSE --> WASM_DISCONNECT
EMIT --> WRITE
WRITE -->|ws.send bytes| MESSAGE
JavaScript Glue Layer
The JavaScript layer must:
- Create and manage a WebSocket connection
- Forward
onopenevents tohandle_connect() - Forward
onmessagedata toread_bytes() - Forward
onerror/oncloseevents tohandle_disconnect() - Implement
muxioWriteBytes()to send data back through the WebSocket
Sources: extensions/muxio-wasm-rpc-client/src/static_lib/static_client.rs:1-8 extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:27-34
Making RPC Calls
The WASM client implements RpcServiceCallerInterface, enabling the same call patterns as the Tokio client. All methods defined in service definitions using RpcMethodPrebuffered are available.
sequenceDiagram
participant WASM as WASM Code
participant Caller as RpcServiceCallerInterface
participant Dispatcher as RpcDispatcher
participant Emit as emit_callback
participant JS as JavaScript
participant WS as WebSocket
participant Server as Server
WASM->>Caller: Add::call(client, params)
Caller->>Dispatcher: encode_request + METHOD_ID
Dispatcher->>Dispatcher: assign request_id
Dispatcher->>Emit: emit_callback(bytes)
Emit->>JS: muxioWriteBytes(bytes)
JS->>WS: websocket.send(bytes)
WS->>Server: transmit
Server->>WS: response
WS->>JS: onmessage(bytes)
JS->>Caller: read_bytes(bytes)
Caller->>Dispatcher: decode frames
Dispatcher->>Dispatcher: match request_id
Dispatcher->>Caller: decode_response
Caller-->>WASM: Result<Response>
Example Usage Pattern
From WASM code:
- Obtain client reference (either directly or via
with_static_client_async) - Call service methods using the trait (e.g.,
Add::call(&client, request).await) - Handle the returned
Result<Response, RpcServiceError>
The client automatically handles:
- Request serialization with
bitcode - METHOD_ID attachment for routing
- Request correlation via dispatcher
- Response deserialization
- Error propagation
Sources: extensions/muxio-wasm-rpc-client/src/lib.rs:6-9 extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:154-181
graph TB
subgraph "JavaScript"
WS["WebSocket\nonmessage(bytes)"]
end
subgraph "RpcWasmClient"
READ["read_bytes(bytes)"]
STAGE1["Stage 1:\nExtract finalized requests"]
STAGE2["Stage 2:\nprocess_single_prebuffered_request()"]
STAGE3["Stage 3:\nrespond(response)"]
end
subgraph "RpcServiceEndpoint"
HANDLERS["get_prebuffered_handlers()"]
DISPATCH["dispatch by METHOD_ID"]
end
subgraph "User Code"
HANDLER["Registered Handler\nasync fn(request) -> response"]
end
WS --> READ
READ --> STAGE1
STAGE1 --> STAGE2
STAGE2 --> HANDLERS
HANDLERS --> DISPATCH
DISPATCH --> HANDLER
HANDLER --> STAGE2
STAGE2 --> STAGE3
STAGE3 -->|emit_callback| WS
Handling Incoming RPC Calls
The WASM client can also act as a server, handling RPC calls initiated by the remote endpoint. This enables bidirectional RPC where both client and server can initiate calls.
sequenceDiagram
participant Code as User Code
participant Client as RpcWasmClient
participant Endpoint as RpcServiceEndpoint
Code->>Client: get_endpoint()
Client-->>Code: Arc<RpcServiceEndpoint<()>>
Code->>Endpoint: register_prebuffered_handler::<Method>()
Note over Endpoint: Store handler by METHOD_ID
Registering Handlers
Handlers are registered with the RpcServiceEndpoint obtained via get_endpoint():
When an incoming request arrives:
read_bytes()extracts the request from the dispatcherprocess_single_prebuffered_request()looks up the handler by METHOD_ID- The handler executes asynchronously
- The response is serialized and sent via
respond()
The context type for WASM client handlers is () since there is no per-connection state.
Sources: extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:86-120 extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:141-143
stateDiagram-v2
[*] --> Disconnected
Disconnected --> Connected : handle_connect()
Connected --> Disconnected : handle_disconnect()
state Connected {
[*] --> Ready
Ready --> Processing : read_bytes()
Processing --> Ready
}
note right of Connected
is_connected = true
emit state_change_handler(Connected)
end note
note right of Disconnected
is_connected = false
emit state_change_handler(Disconnected)
fail_all_pending_requests()
end note
State Management
The WASM client tracks connection state using an AtomicBool and provides optional state change notifications.
State Change Handler
Applications can register a callback to receive notifications when the connection state changes:
| State | Trigger | Actions |
|---|---|---|
Connected | handle_connect() called | Handler invoked with RpcTransportState::Connected |
Disconnected | handle_disconnect() called | Handler invoked with RpcTransportState::Disconnected, all pending requests failed |
Sources: extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:168-180 extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:22-23
Dependencies
The WASM client has minimal dependencies focused on WASM/JavaScript interop:
| Dependency | Purpose |
|---|---|
wasm-bindgen | JavaScript/Rust FFI bindings |
wasm-bindgen-futures | Convert Rust futures to JavaScript promises |
js-sys | JavaScript standard library types |
tokio | Async runtime (sync primitives only: Mutex) |
futures | Future composition utilities |
muxio | Core multiplexing and framing protocol |
muxio-rpc-service | RPC trait definitions and METHOD_ID generation |
muxio-rpc-service-caller | Client-side RPC call interface |
muxio-rpc-service-endpoint | Server-side RPC handler interface |
Note: While tokio is included, the WASM client does not use Tokio's runtime. Only synchronization primitives like Mutex are used, which work in WASM environments.
Sources: extensions/muxio-wasm-rpc-client/Cargo.toml:11-22
Thread Safety
The WASM client is designed for single-threaded WASM environments:
Arcis used for reference counting, but WASM is single-threadedMutexguards shared state but never blocks (no contention)AtomicBoolprovides lock-free state access- All callbacks use
Send + Syncbounds for API consistency with native code
The three-stage read_bytes() pipeline ensures the dispatcher lock is held only during brief serialization/deserialization operations, not during handler execution.
Sources: extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:46-121 extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:7-14
Comparison with Tokio Client
| Feature | WASM Client | Tokio Client |
|---|---|---|
| WebSocket Management | Delegated to JavaScript | Built-in with tokio-tungstenite |
| Event Model | Callback-based (onopen, onmessage, etc.) | Async stream-based |
| Connection Initialization | handle_connect() | connect() |
| Data Reading | read_bytes() called from JS | read_loop() task |
| Async Runtime | None (WASM environment) | Tokio |
| State Tracking | AtomicBool + manual calls | Automatic with connection task |
| Bidirectional RPC | Yes, via RpcServiceEndpoint | Yes, via RpcServiceEndpoint |
| Static Client Pattern | Yes, via thread_local | Not applicable |
Both clients implement RpcServiceCallerInterface, ensuring identical call patterns and service definitions work across both environments.
Sources: extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:16-35
Dismiss
Refresh this wiki
Enter email to refresh