Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

GitHub

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

Loading…

WASM RPC Client

Relevant source files

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 bridges Rust WASM code with JavaScript’s WebSocket API. Unlike the Tokio client which manages its own WebSocket connection, RpcWasmClient relies on JavaScript glue code to handle WebSocket events and delegates to Rust for RPC protocol processing.

Diagram: WASM Client Architecture and Data Flow

graph TB
    subgraph "Browser JavaScript"
        WS[WebSocket]
        WRITE_BYTES["static_muxio_write_bytes()"]
APP["Web Application"]
end
    
    subgraph "WASM Module"
        TLS["MUXIO_STATIC_RPC_CLIENT_REF\nRefCell<Option<Arc<RpcWasmClient>>>"]
CLIENT["RpcWasmClient"]
DISP["Arc<Mutex<RpcDispatcher>>"]
EP["Arc<RpcServiceEndpoint<()>>"]
EMIT["emit_callback: Arc<dyn Fn(Vec<u8>)>"]
CONN["is_connected: Arc<AtomicBool>"]
end
    
    subgraph "Core Layer"
        MUXIO["muxio::rpc::RpcDispatcher\nmuxio::frame"]
end
    
 
   APP -->|new WebSocket| WS
 
   WS -->|onopen| TLS
 
   WS -->|onmessage bytes| TLS
 
   WS -->|onerror/onclose| TLS
    
 
   TLS -->|handle_connect| CLIENT
 
   TLS -->|read_bytes bytes| CLIENT
 
   TLS -->|handle_disconnect| CLIENT
    
 
   CLIENT --> DISP
 
   CLIENT --> EP
 
   CLIENT --> EMIT
 
   CLIENT --> CONN
    
 
   EMIT -->|invoke| WRITE_BYTES
 
   WRITE_BYTES -->|websocket.send| WS
    
 
   DISP --> MUXIO
 
   EP --> MUXIO

The architecture consists of three layers:

  1. JavaScript Layer : Manages WebSocket lifecycle (onopen, onmessage, onclose) and forwards events to WASM
  2. WASM Bridge Layer : RpcWasmClient with emit_callback for outbound data and lifecycle methods for inbound events
  3. Core RPC Layer : RpcDispatcher for request/response correlation and RpcServiceEndpoint<()> for 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:1-11 extensions/muxio-wasm-rpc-client/src/static_lib/static_client.rs:25-36

RpcWasmClient Structure

The RpcWasmClient struct manages bidirectional RPC communication in WASM environments. It implements RpcServiceCallerInterface for outbound calls and uses RpcServiceEndpoint<()> for inbound request handling.

Diagram: RpcWasmClient Class Structure

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: &[u8]) async
        +handle_disconnect() async
        +is_connected() bool
        +get_endpoint() Arc~RpcServiceEndpoint~()~~
        -dispatcher() Arc~Mutex~RpcDispatcher~~
        -emit() Arc~dyn Fn(Vec~u8~)~
    }
    
    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: &[u8]) Result~Vec~u32~, FrameDecodeError~
        +respond(response, chunk_size, callback) Result
        +is_rpc_request_finalized(id: u32) Option~bool~
        +delete_rpc_request(id: u32) Option~RpcRequest~
        +fail_all_pending_requests(error)
    }
    
    class RpcServiceEndpoint {+get_prebuffered_handlers() Arc\n+register_prebuffered_handler()}
    
    RpcWasmClient ..|> RpcServiceCallerInterface
    RpcWasmClient --> RpcDispatcher
    RpcWasmClient --> RpcServiceEndpoint
FieldTypePurpose
dispatcherArc<Mutex<RpcDispatcher<'static>>>Manages request/response correlation via request_id and stream multiplexing
endpointArc<RpcServiceEndpoint<()>>Dispatches incoming RPC requests to registered handlers by METHOD_ID
emit_callbackArc<dyn Fn(Vec<u8>) + Send + Sync>Callback invoked to send bytes to JavaScript’s static_muxio_write_bytes()
state_change_handlerArc<Mutex<Option<Box<dyn Fn(RpcTransportState) + Send + Sync>>>>Optional callback for Connected/Disconnected state transitions
is_connectedArc<AtomicBool>Lock-free connection status tracking

Sources: extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:16-35 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. Sets is_connected to true via AtomicBool::store() and invokes the registered state_change_handler with RpcTransportState::Connected.

Sources: extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:38-44

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.

Diagram: read_bytes Three-Stage Pipeline

sequenceDiagram
    participant JS as "JavaScript\nonmessage"
    participant RB as "read_bytes()"
    participant Disp as "Mutex<RpcDispatcher>"
    participant Proc as "process_single_prebuffered_request()"
    participant Handler as "User Handler"
    
    JS->>RB: read_bytes(bytes: &[u8])
    
    rect rgb(240, 240, 240)
    Note over RB,Disp: Stage 1: Synchronous Reading (lines 56-81)
    RB->>Disp: lock().await
    RB->>Disp: dispatcher.read_bytes(bytes)
    Disp-->>RB: Ok(request_ids: Vec<u32>)
    loop "for id in request_ids"
        RB->>Disp: is_rpc_request_finalized(id)
        Disp-->>RB: Some(true)
        RB->>Disp: delete_rpc_request(id)
        Disp-->>RB: Some(RpcRequest)
    end
    Note over RB: Lock dropped here
    end
    
    rect rgb(245, 245, 245)
    Note over RB,Handler: Stage 2: Async Processing (lines 83-103)
    loop "for (request_id, request)"
        RB->>Proc: process_single_prebuffered_request()
        Proc->>Handler: handler(context, request)
        Handler-->>Proc: Result<Vec<u8>, RpcServiceError>
        Proc-->>RB: RpcResponse
    end
    RB->>RB: join_all(response_futures).await
    end
    
    rect rgb(240, 240, 240)
    Note over RB,JS: Stage 3: Synchronous Sending (lines 105-120)
    RB->>Disp: lock().await
    loop "for response"
        RB->>Disp: dispatcher.respond(response, chunk_size, callback)
        Disp->>RB: emit_callback(chunk)
        RB->>JS: static_muxio_write_bytes(chunk)
    end
    Note over RB: Lock dropped here
    end

Stage 1 (lines 56-81) : Acquires dispatcher lock via Mutex::lock().await, calls dispatcher.read_bytes(bytes) to decode frames, identifies finalized requests using is_rpc_request_finalized(), extracts them with delete_rpc_request(), then releases lock.

Stage 2 (lines 83-103) : Without holding any locks, calls process_single_prebuffered_request() for each request. This invokes user handlers asynchronously and collects RpcResponse results using join_all().

Stage 3 (lines 105-120) : Re-acquires dispatcher lock, calls dispatcher.respond() for each response, which invokes emit_callback synchronously to send chunks via static_muxio_write_bytes().

This three-stage design prevents deadlocks by releasing the lock during handler execution and enables concurrent request processing.

Sources: extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:48-121

handle_disconnect

Called when JavaScript’s WebSocket onclose or onerror events fire. Uses AtomicBool::swap() to atomically set is_connected to false, invokes the state_change_handler with RpcTransportState::Disconnected, and calls dispatcher.fail_all_pending_requests() with FrameDecodeError::ReadAfterCancel to terminate all in-flight RPC calls.

Sources: extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:124-134

Static Client Pattern

For simplified JavaScript integration, the WASM client provides a static client pattern using thread-local storage via MUXIO_STATIC_RPC_CLIENT_REF. This eliminates the need to pass client instances through JavaScript’s FFI boundary.

Diagram: Static Client Initialization and Access Flow

graph TB
    subgraph "JavaScript"
        INIT["init()"]
CALL["callRpcMethod()"]
end
    
    subgraph "WASM Exports"
        INIT_EXPORT["#[wasm_bindgen]\ninit_static_client()"]
RPC_EXPORT["#[wasm_bindgen]\nexported_rpc_fn()"]
end
    
    subgraph "Static Client Layer"
        TLS["MUXIO_STATIC_RPC_CLIENT_REF\nthread_local!\nRefCell<Option<Arc<RpcWasmClient>>>"]
WITH["with_static_client_async()"]
GET["get_static_client()"]
end
    
    subgraph "Client"
        CLIENT["Arc<RpcWasmClient>"]
end
    
 
   INIT --> INIT_EXPORT
 
   INIT_EXPORT -->|cell.borrow_mut| TLS
 
   TLS -.->|stores| CLIENT
    
 
   CALL --> RPC_EXPORT
 
   RPC_EXPORT --> WITH
 
   WITH -->|cell.borrow .clone| TLS
 
   TLS -.->|retrieves| CLIENT
 
   WITH -->|FnOnce Arc<RpcWasmClient>| CLIENT

init_static_client

Initializes MUXIO_STATIC_RPC_CLIENT_REF thread-local storage with Arc<RpcWasmClient>. The function is idempotent—subsequent calls have no effect. The client is constructed with RpcWasmClient::new(|bytes| static_muxio_write_bytes(&bytes)) to bridge outbound data to JavaScript.

Sources: extensions/muxio-wasm-rpc-client/src/static_lib/static_client.rs:25-36 extensions/muxio-wasm-rpc-client/src/static_lib/static_client.rs:9-11

with_static_client_async

Primary method for interacting with the static client from #[wasm_bindgen] exported functions. Retrieves Arc<RpcWasmClient> from MUXIO_STATIC_RPC_CLIENT_REF.with(), invokes the provided closure, and converts the result to a JavaScript Promise via future_to_promise().

ParameterTypeDescription
fFnOnce(Arc<RpcWasmClient>) -> Fut + 'staticClosure receiving client reference
FutFuture<Output = Result<T, String>> + 'staticFuture returned by closure
TInto<JsValue>Result type convertible to JavaScript value
ReturnsPromiseJavaScript promise resolving to T or rejecting with error string

If the static client has not been initialized, the promise rejects with "RPC client not initialized".

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. The integration relies on the emit_callback mechanism for outbound data and lifecycle methods for inbound events.

Diagram: JavaScript-WASM Bridge

graph TB
    subgraph "JavaScript WebSocket Events"
        OPEN["ws.onopen"]
MESSAGE["ws.onmessage"]
ERROR["ws.onerror"]
CLOSE["ws.onclose"]
end
    
    subgraph "WASM Exported Functions"
        WASM_CONNECT["handle_connect()"]
WASM_READ["read_bytes(event.data)"]
WASM_DISCONNECT["handle_disconnect()"]
end
    
    subgraph "WASM Emit Path"
        EMIT["emit_callback(bytes: Vec<u8>)"]
STATIC_WRITE["static_muxio_write_bytes(&bytes)"]
end
    
    subgraph "JavaScript Bridge"
        WRITE_FN["muxioWriteBytes(bytes)"]
end
    
 
   OPEN -->|await| WASM_CONNECT
 
   MESSAGE -->|await read_bytes new Uint8Array| WASM_READ
 
   ERROR -->|await| WASM_DISCONNECT
 
   CLOSE -->|await| WASM_DISCONNECT
    
 
   EMIT -->|invoke| STATIC_WRITE
 
   STATIC_WRITE -->|#[wasm_bindgen]| WRITE_FN
 
   WRITE_FN -->|websocket.send bytes| MESSAGE

The JavaScript layer must:

  1. Create and manage a WebSocket instance
  2. Forward onopen events to await handle_connect()
  3. Forward onmessage data to await read_bytes(new Uint8Array(event.data))
  4. Forward onerror/onclose events to await handle_disconnect()
  5. Implement muxioWriteBytes() function to receive data from static_muxio_write_bytes() and call websocket.send(bytes)

The emit_callback is constructed with |bytes| static_muxio_write_bytes(&bytes) when creating the client, which bridges to the JavaScript muxioWriteBytes() function.

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 using RpcMethodPrebuffered trait in service definitions are available via call_rpc_buffered().

Diagram: Outbound RPC Call Flow

sequenceDiagram
    participant WASM as "WASM Code"
    participant Method as "Add::call()"
    participant Caller as "RpcServiceCallerInterface"
    participant Disp as "RpcDispatcher"
    participant Emit as "emit_callback"
    participant JS as "static_muxio_write_bytes()"
    participant WS as "WebSocket"
    
    WASM->>Method: Add::call(&client, request).await
    Method->>Method: encode_request(params)\nwith METHOD_ID
    Method->>Caller: call_rpc_buffered(RpcRequest)
    Caller->>Disp: get_dispatcher().lock()
    Disp->>Disp: assign request_id
    Disp->>Disp: encode frames
    Disp->>Emit: get_emit_fn()(bytes)
    Emit->>JS: static_muxio_write_bytes(&bytes)
    JS->>WS: websocket.send(bytes)
    
    WS->>JS: onmessage(response_bytes)
    JS->>Caller: read_bytes(response_bytes)
    Caller->>Disp: decode frames
    Disp->>Disp: match request_id
    Disp->>Method: decode_response(bytes)
    Method-->>WASM: Result<Response, RpcServiceError>

Call Mechanics

From WASM code, RPC calls follow this pattern:

  1. Obtain Arc<RpcWasmClient> via with_static_client_async() or direct reference
  2. Call service methods: SomeMethod::call(&client, request).await
  3. The trait implementation calls client.call_rpc_buffered() which:
    • Serializes the request with bitcode::encode()
    • Attaches METHOD_ID constant in the RpcHeader
    • Invokes dispatcher.call() with a unique request_id
    • Emits encoded frames via emit_callback
  4. Awaits response correlation by request_id in the dispatcher
  5. Returns Result<DecodedResponse, RpcServiceError>

Sources: extensions/muxio-wasm-rpc-client/src/lib.rs:6-9 extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:154-167

graph TB
    subgraph "JavaScript"
        WS["WebSocket.onmessage"]
end
    
    subgraph "read_bytes()
Pipeline"
        STAGE1["Stage 1:\ndelete_rpc_request(id)"]
STAGE2["Stage 2:\nprocess_single_prebuffered_request()"]
STAGE3["Stage 3:\ndispatcher.respond()"]
end
    
    subgraph "RpcServiceEndpoint<()>"
        HANDLERS["get_prebuffered_handlers()\nHashMap<u32, Box<Handler>>"]
LOOKUP["lookup by METHOD_ID"]
end
    
    subgraph "User Handler"
        HANDLER["async fn(context: (), request: Vec<u8>)\n-> Result<Vec<u8>, RpcServiceError>"]
end
    
 
   WS -->|bytes| STAGE1
 
   STAGE1 -->|Vec< u32, RpcRequest >| STAGE2
 
   STAGE2 --> HANDLERS
 
   HANDLERS --> LOOKUP
 
   LOOKUP --> HANDLER
 
   HANDLER -->|Result| STAGE2
 
   STAGE2 -->|Vec<RpcResponse>| STAGE3
 
   STAGE3 -->|emit_callback| WS

Handling Incoming RPC Calls

The WASM client supports bidirectional RPC by handling incoming calls from the server. The RpcServiceEndpoint<()> dispatches requests to registered handlers by METHOD_ID.

Diagram: Inbound RPC Request Processing

sequenceDiagram
    participant Code as "User Code"
    participant Client as "RpcWasmClient"
    participant EP as "RpcServiceEndpoint<()>"
    participant Map as "HashMap<u32, Handler>"
    
    Code->>Client: get_endpoint()
    Client-->>Code: Arc<RpcServiceEndpoint<()>>
    Code->>EP: register_prebuffered_handler::<Method>(handler)
    EP->>Map: insert(Method::METHOD_ID, Box<handler>)
    Note over Map: Handler stored for Method::METHOD_ID

Registering Handlers

Handlers are registered with the RpcServiceEndpoint<()> obtained via get_endpoint():

Diagram: Handler Registration Flow

When an incoming request arrives in read_bytes():

  1. Stage 1 : dispatcher.delete_rpc_request(id) extracts the RpcRequest containing METHOD_ID in its header
  2. Stage 2 : process_single_prebuffered_request() looks up the handler via get_prebuffered_handlers() using METHOD_ID
  3. Handler executes: handler(context: (), request.rpc_prebuffered_payload_bytes)
  4. Stage 3 : dispatcher.respond() serializes the response and invokes emit_callback

The context type for WASM client handlers is () since there is no per-connection state in WASM environments.

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:

StateTriggerActions
Connectedhandle_connect() calledHandler invoked with RpcTransportState::Connected
Disconnectedhandle_disconnect() calledHandler 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:

DependencyVersionPurpose
wasm-bindgen0.2.100JavaScript/Rust FFI bindings via #[wasm_bindgen]
wasm-bindgen-futures0.4.50Convert Rust Future to JavaScript Promise via future_to_promise()
js-sys0.3.77JavaScript standard library types (Promise, Uint8Array)
tokioworkspaceAsync runtime (only tokio::sync::Mutex used, not the executor)
futuresworkspaceFuture composition (join_all() for concurrent request processing)
async-traitworkspaceAsync trait implementations (#[async_trait] for RpcServiceCallerInterface)
muxioworkspaceCore multiplexing (RpcDispatcher, RpcSession, frame encoding)
muxio-rpc-serviceworkspaceRPC trait definitions (RpcMethodPrebuffered, METHOD_ID)
muxio-rpc-service-callerworkspaceRpcServiceCallerInterface trait
muxio-rpc-service-endpointworkspaceRpcServiceEndpoint, process_single_prebuffered_request()
tracingworkspaceLogging macros (tracing::error!)

Note: While tokio is included, the WASM client does not use Tokio’s executor. Only synchronization primitives like tokio::sync::Mutex are used, which work in single-threaded WASM environments.

Sources: extensions/muxio-wasm-rpc-client/Cargo.toml:11-22

Thread Safety and Concurrency

The WASM client is designed for single-threaded WASM environments but uses thread-safe primitives for API consistency with native code:

PrimitivePurposeWASM Behavior
Arc<T>Reference counting for shared ownershipWorks in single-threaded context, no actual atomics needed
tokio::sync::Mutex<RpcDispatcher>Guards dispatcher state during frame encoding/decodingNever contends (single-threaded), provides interior mutability
Arc<AtomicBool>Lock-free is_connected trackingload()/store()/swap() operations work without OS threads
Send + Sync boundsTrait bounds on callbacks and handlersSatisfied for API consistency, no actual thread migration

The three-stage read_bytes() pipeline ensures the dispatcher lock is held only during:

  • Stage 1: read_bytes(), is_rpc_request_finalized(), delete_rpc_request() (lines 58-81)
  • Stage 3: respond() calls (lines 108-119)

Lock is not held during Stage 2’s async handler execution (lines 85-103), enabling concurrent request processing via join_all().

Sources: extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:48-121 extensions/muxio-wasm-rpc-client/src/rpc_wasm_client.rs:7-14

Comparison with Tokio Client

FeatureWASM ClientTokio Client
WebSocket ManagementDelegated to JavaScriptBuilt-in with tokio-tungstenite
Event ModelCallback-based (onopen, onmessage, etc.)Async stream-based
Connection Initializationhandle_connect()connect()
Data Readingread_bytes() called from JSread_loop() task
Async RuntimeNone (WASM environment)Tokio
State TrackingAtomicBool + manual callsAutomatic with connection task
Bidirectional RPCYes, via RpcServiceEndpointYes, via RpcServiceEndpoint
Static Client PatternYes, via thread_localNot 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