infinity-bridge-wire 0.1.2

Wire protocol types for infinity-bridge — shared between WASM and host crates
Documentation

infinity-wasm-bridge

A three-layer bridge that lets an MSFS WASM gauge send and receive data to an external host application over WebSocket. WASM gauges run in a sandbox and cannot open TCP sockets directly — this library routes messages through the adjacent Coherent HTML gauge engine via the CommBus IPC mechanism.

Performance

This bridge is not the bottleneck.

  • ~20–40 ms latency floor (hard limit of MSFS WASM tick rate)
  • Fully saturates WASM throughput
  • ~25,000 events/sec (burst)
  • ~12–13 MB/s sustained
  • Zero drops under load
  • Linear scaling with concurrency
  • Flat latency across large payloads (100s KB+) with binary transport

Reality

The only thing limiting performance is MSFS itself.

  • ~20 ms ≈ 1 WASM tick
  • ~40 ms ≈ round trip
  • Same limitation applies to SimConnect in WASM

Binary Transport

  • No JSON on the hot path
  • No reserialization overhead
  • Stable latency regardless of payload size
  • Large payloads without degradation

Bottom Line

infinity-wasm-bridge fully saturates MSFS WASM performance while delivering larger payloads with a modern, seamless API when compared to SimConnect.

You are not limited by transport. You are limited by the sim.

 ┌─────────────────────────────── MSFS Process ──────────────────────────────────┐
 │                                                                                │
 │  ┌─────────────────────┐    CommBus JSON    ┌──────────────────────────────┐  │
 │  │  infinity-bridge-wasm   │ ◄────────────────► │  infinity-bridge-relay (JS/TS)   │  │
 │  │  (Rust WASM gauge)  │                    │  (Coherent HTML gauge)       │  │
 │  └─────────────────────┘                    └──────────┬───────────────────┘  │
 │                                                        │ WebSocket             │
 └────────────────────────────────────────────────────────┼───────────────────────┘
                                                          │
                                              ┌───────────▼───────────────────┐
                                              │  infinity-bridge-host (Rust/Tokio) │
                                              │  WebSocket server (axum)       │
                                              └───────────────────────────────┘

Crates

Crate Description
infinity-bridge-wire Shared wire format types (no_std compatible)
infinity-bridge-wasm WASM gauge side — CommBus abstraction and command router
infinity-bridge-host Host application side — async WebSocket server
infinity-bridge-relay TypeScript relay running inside the Coherent HTML gauge

Quick Start

1. Host Application

Add the dependency:

[dependencies]
infinity-bridge-host = "0.1.0"
tokio = { version = "1", features = ["full"] }

Start the server and interact with the gauge:

use infinity_bridge_host::{BridgeServer, ServerConfig};
use serde_json::json;
use std::time::Duration;

#[tokio::main]
async fn main() {
    let config = ServerConfig::new("127.0.0.1:9876", "/bridge");
    let server = BridgeServer::start(config).await.unwrap();

    // Wait for the gauge to connect
    server.wait_connected().await;

    // Send a command and await the response
    let response = server
        .command("equip_query", json!({}), Duration::from_secs(3))
        .await
        .unwrap();
    println!("{response:#}");

    // Subscribe to events sent from the WASM gauge
    let mut events = server.subscribe_events();
    tokio::spawn(async move {
        while let Ok(event) = events.recv().await {
            println!("gauge event: {}{:?}", event.name, event.data);
        }
    });

    // Fire a one-way event toward the gauge
    server.emit("config_updated", json!({"livery": "American Airlines"})).unwrap();
}

2. TypeScript Relay (Coherent HTML Gauge)

Install the relay package into your HTML gauge project, then initialize it in your BaseInstrument:

import { BridgeRelay } from 'infinity-bridge-relay';

export class MyGauge extends BaseInstrument {
    private relay!: BridgeRelay;

    connectedCallback(): void {
        super.connectedCallback();
        this.relay = new BridgeRelay({
            wsUrl: "ws://127.0.0.1:9876/bridge",
            callEvent: "myaddon/bridge_call",
            responseEvent: "myaddon/bridge_resp",
            hello: { client: "msfs-gauge", aircraft: "DC-10-30" },
        });
        this.relay.init();
    }

    Update(): void {
        super.Update();
        this.relay.update();
    }
}

3. WASM Gauge (Rust)

The WASM crate has no dependency on the MSFS SDK — you provide a thin CommBusBackend impl:

use infinity_bridge_wasm::{CommBusBackend, BridgeConfig, Bridge, Router};

// Wire up your msfs crate version
struct MyBackend;

impl CommBusBackend for MyBackend {
    type Error = msfs::MSFSError;
    type Subscription = msfs::sys::CommBusSubscription;

    fn subscribe(event: &str, cb: impl Fn(&str) + 'static) -> Result<Self::Subscription, Self::Error> {
        msfs::commbus::CommBus::subscribe(event, cb)
    }
    fn call(event: &str, data: &str) -> Result<(), Self::Error> {
        msfs::commbus::CommBus::call(event, data)
    }
}

// Set up the router — event names must match the relay config
let config = BridgeConfig::new("myaddon/bridge_call", "myaddon/bridge_resp");

let router = Router::new()
    .command("equip_query", move |_| {
        Ok(json!({"entries": ["item_a", "item_b"]}))
    })
    .command("set_config", move |payload| {
        // apply payload to state...
        Ok(json!({"ok": true}))
    })
    .event("config_updated", move |data| {
        println!("config changed: {data:?}");
    });

let bridge = Bridge::<MyBackend>::new(config, router)?;

// Later, emit a fire-and-forget event to the host
bridge.emit("telemetry", json!({"altitude_ft": 35_000, "speed_kts": 285}))?;

Architecture

Wire Protocol

All messages are JSON with a "t" discriminant field. Defined in infinity-bridge-wire and mirrored in the TypeScript relay.

Message Direction Description
hello Gauge → Host First message after WebSocket connect — identifies the client
ping Host → Gauge Keepalive probe
pong Gauge → Host Keepalive response
cmd Host → Gauge RPC request with a correlation UUID
ack Gauge → Host RPC response (success or error)
event Either direction Fire-and-forget named event
{"t":"hello","client":"msfs-gauge","aircraft":"DC-10-30","tail":"N1819U","session":"1234567","v":1}
{"t":"ping","ts":1711234567890}
{"t":"pong","ts":1711234567890}
{"t":"cmd","id":"550e8400-e29b-41d4-a716-446655440000","name":"equip_query","payload":{}}
{"t":"ack","id":"550e8400-e29b-41d4-a716-446655440000","ok":true,"response":{"entries":[]}}
{"t":"ack","id":"...","ok":false,"error":"equipment idx 5 not found"}
{"t":"event","name":"telemetry","data":{"altitude_ft":35000,"speed_kts":285}}

Data Flow: Host → WASM (command/RPC)

BridgeServer::command("equip_query", payload, timeout)
  → UUID assigned, oneshot registered in pending map
  → WireMsg::Cmd serialized → WebSocket → JS relay
      → dedup check (duplicate IDs replied to immediately)
      → CommBus.call(callEvent, {requestId, payload})
          → Bridge<B>::dispatch (sync WASM callback)
              → Router matches handler, returns Result<Value>
              → B::call(responseEvent, {requestId, ok, response})
          → JS receives CommBus response
          → WireMsg::Ack sent over WebSocket
  → Hub resolves oneshot → caller receives Ok(Value) or Err

Data Flow: WASM → Host (fire-and-forget event)

bridge.emit("telemetry", json!({...}))
  → WireMsg::Event serialized
  → B::call(responseEvent, json)       // direct CommBus, no requestId
      → JS relay: t === "event" → forwards raw over WebSocket
          → Hub.dispatch_event
              → event_tx.send(EventPayload)
                  → all broadcast::Receiver<EventPayload> subscribers notified

Deduplication

The TypeScript relay maintains a bounded LRU ring (DedupRing, default capacity 128) keyed on command IDs. If a command is retransmitted before an ack is sent (e.g. during reconnect), the relay responds immediately with {ok: true, duplicate: true} without forwarding to the WASM side.

Reconnection

The relay reconnects to the WebSocket server automatically using exponential backoff with jitter: min(maxReconnectMs, baseReconnectMs × 2^attempt) + rand(0..250ms). Defaults: base 250 ms, max 30 s.

Timeout ordering

There are two timeouts on every command — the host's (BridgeServer::command) and the relay's (requestTimeoutMs) — and the relay's must be the shorter of the two.

The relay is the only party that can tell you why a command went unanswered: whether the WASM module never replied, or its CommBus listener was never bound in the first place. It reports that by acking with an error. If the host's timeout fires first, that ack arrives to a request that no longer exists, the diagnosis is discarded, and every distinct cause reaches the caller as one indistinguishable TIMEOUT.

The relay defaults to 2500 ms so it fits inside a 3 s host timeout out of the box. Raise the host's first if you need longer.

Readiness

An open socket proves the relay is alive. It does not prove anything behind it is: in MSFS the Coherent panel hosting the relay loads well before the aircraft's WASM modules exist, so there is a window — seconds, sometimes longer on a cold load — where the socket is up and no command can possibly be served.

The relay emits __bridge_ready (infinity_bridge_wire::READY_EVENT) as soon as its CommBus listener is bound, and again on every reconnect. The host consumes it rather than fanning it out to event subscribers, prefers ready clients when dispatching a command, and exposes BridgeServer::is_ready() alongside is_connected().

Readiness is a positive signal only. Relays predating the event never send one, so the host falls back to broadcasting to every client when nothing has reported ready. Treat is_ready() == false as "not known to be reachable", never as "unreachable".


Configuration Reference

ServerConfig (host)

ServerConfig::new("127.0.0.1:9876", "/bridge")
    .ping_interval(Duration::from_secs(10))
    .ping_timeout(Duration::from_secs(30))
    .event_channel_capacity(256)

BridgeRelayConfig (TypeScript)

{
    wsUrl: string;            // WebSocket URL of the host server
    callEvent: string;        // CommBus event name used to call into WASM
    responseEvent: string;    // CommBus event name WASM responds on
    hello?: {                 // Optional handshake metadata
        client?: string;
        aircraft?: string;
        tail?: string;
        session?: string;
        meta?: unknown;
    };
    dedupCapacity?: number;   // Default 128
    baseReconnectMs?: number; // Default 250
    maxReconnectMs?: number;  // Default 30_000
    protocolVersion?: number; // Default 1
    requestTimeoutMs?: number; // Default 2500 — MUST stay below the host's
    bindRetryMs?: number;      // Default 1000 — CommBus bind retry cadence
}

The ReScript relay (@infinity-msfs/rescript-bridge-relay) takes the same fields.

BridgeConfig (WASM)

BridgeConfig::new("myaddon/bridge_call", "myaddon/bridge_resp")

The CommBus event names must be consistent across all three layers.


Integrating with Tauri

BridgeServer can be mounted inside an existing axum router, making it straightforward to embed inside a Tauri app:

let (bridge, bridge_router) = BridgeServer::router(config);
let app = axum::Router::new()
    .merge(bridge_router)
    .route("/api/health", get(|| async { "ok" }));

// Forward gauge events to the Tauri webview
let mut events = bridge.subscribe_events();
let handle = app_handle.clone();
tokio::spawn(async move {
    while let Ok(event) = events.recv().await {
        handle.emit(&event.name, &event.data).ok();
    }
});

Examples

File Description
examples/host-app/src/main.rs Full host-side demonstration — commands, events, and error handling
examples/host-app/src/fake_gauge.rs Simulates the relay + WASM pipeline as a plain WebSocket client for testing
examples/wasm_gauge_example.rs Complete WASM integration reference (requires MSFS SDK at build time)
examples/cohierent.ts TypeScript relay usage inside a BaseInstrument

Run the bundled host example (starts server + fake gauge in the same process):

cargo run --example host-app