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

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 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:

  1. JavaScript Layer : Manages WebSocket lifecycle and forwards events to WASM
  2. WASM Bridge Layer : RpcWasmClient and static client helpers
  3. Core RPC Layer : RpcDispatcher for multiplexing 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: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
FieldTypePurpose
dispatcherArc<Mutex<RpcDispatcher>>Manages request/response correlation and stream multiplexing
endpointArc<RpcServiceEndpoint<()>>Handles incoming RPC requests from the server
emit_callbackArc<dyn Fn(Vec<u8>)>Callback to send bytes to JavaScript WebSocket
state_change_handlerArc<Mutex<Option<Box<dyn Fn(RpcTransportState)>>>>Optional callback for connection state changes
is_connectedArc<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&lt;RpcWasmClient&gt;"]
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.

ParameterTypeDescription
fFnOnce(Arc<RpcWasmClient>) -> FutClosure receiving client reference
FutFuture<Output = Result<T, String>>Future returned by closure
TInto<JsValue>Result type convertible to JavaScript value
ReturnsPromiseJavaScript 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:

  1. Create and manage a WebSocket connection
  2. Forward onopen events to handle_connect()
  3. Forward onmessage data to read_bytes()
  4. Forward onerror/onclose events to handle_disconnect()
  5. 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:

  1. Obtain client reference (either directly or via with_static_client_async)
  2. Call service methods using the trait (e.g., Add::call(&client, request).await)
  3. 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:

  1. read_bytes() extracts the request from the dispatcher
  2. process_single_prebuffered_request() looks up the handler by METHOD_ID
  3. The handler executes asynchronously
  4. 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:

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:

DependencyPurpose
wasm-bindgenJavaScript/Rust FFI bindings
wasm-bindgen-futuresConvert Rust futures to JavaScript promises
js-sysJavaScript standard library types
tokioAsync runtime (sync primitives only: Mutex)
futuresFuture composition utilities
muxioCore multiplexing and framing protocol
muxio-rpc-serviceRPC trait definitions and METHOD_ID generation
muxio-rpc-service-callerClient-side RPC call interface
muxio-rpc-service-endpointServer-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:

  • Arc is used for reference counting, but WASM is single-threaded
  • Mutex guards shared state but never blocks (no contention)
  • AtomicBool provides lock-free state access
  • All callbacks use Send + Sync bounds 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

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

Dismiss

Refresh this wiki

Enter email to refresh