wasm.mdx

May 26, 2026 · View on GitHub

Use WasmTransport for device access without Intiface Central, such as a static web app that pairs over the browser's Bluetooth picker.

import { ButtplugClient } from "@zendrex/buttplug.js";
import { WasmTransport } from "@zendrex/buttplug.js/wasm";

const client = new ButtplugClient(new WasmTransport());
await client.connect();
await client.startScanning(); // opens the Bluetooth device picker

After connect(), the Device API, events, and commands are identical to the WebSocket path. Only transport setup and deployment constraints differ.

Live demo

Try the in-browser WASM server from this page. You need a Chromium browser (Chrome, Edge, or Opera) on HTTPS or localhost, plus a Bluetooth device the browser can pair with.

  1. Connect: downloads buttplug-wasm-blob, starts the in-process server, and completes the handshake.
  2. Scan: opens the system Bluetooth picker to pair a device.
  3. Vibrate 50%: runs a short test command on connected vibrators.

The event log shows the same events you would handle in app code (connection.connected, device.added, …).

WebSocket vs WASM

WebSocket + IntifaceWASM + Web Bluetooth
RuntimeNode, Bun, Deno, browsersChromium browsers
ServerIntiface Central (or other)In-page WASM binary
DiscoveryUSB, serial, BLE relay, etc.Browser Bluetooth picker only
Best forDesktop tools, servers, broad hardwareWeb-only experiences

Need USB-only or relayed devices? Use WebSocket with Intiface. Need a hosted web UI with no desktop install? Use WASM.

Install

Peer dependency on the WASM blob package:

Basic usage

import { ButtplugClient } from "@zendrex/buttplug.js";
import { WasmTransport } from "@zendrex/buttplug.js/wasm";

const client = new ButtplugClient(new WasmTransport());

client.on("device.added", async ({ data: { device } }) => {
  if (device.canOutput("Vibrate")) {
    await device.vibrate(0.5);
  }
});

await client.connect();
await client.startScanning();

connect() lazy-loads the WASM binary on first use, starts the in-process server, and completes the handshake. Subsequent connects reuse the loaded module.

Transport options

import { consoleLogger } from "@zendrex/buttplug.js";
import { WasmTransport } from "@zendrex/buttplug.js/wasm";

const transport = new WasmTransport({
  enableLogging: true,
  logLevel: "debug",
  logger: consoleLogger,
});

const client = new ButtplugClient(transport);

Bundler setup

The root @zendrex/buttplug.js entry does not bundle the WASM blob. Import @zendrex/buttplug.js/wasm only in code paths that need it; treat it as a code-split boundary.

Vite: pre-bundle the peer for faster dev cold starts:

// vite.config.ts
import { defineConfig } from "vite";

export default defineConfig({
  optimizeDeps: {
    include: ["buttplug-wasm-blob"],
  },
});

Standalone example

The same flow ships as a minimal Vite app under example/wasm/:

bun install
bun run example:wasm

Open http://localhost:5173 in Chrome. Useful when iterating on transport code outside the docs site.

Scanning behavior

Web Bluetooth has no continuous discovery: navigator.bluetooth.requestDevice() opens a one-shot system picker. That changes how startScanning() behaves compared to Intiface:

Intiface (WebSocket)WASM (Web Bluetooth)
TriggerServer actively scans backendsBrowser opens device picker
DurationUntil stopScanning() or server timeoutUntil user picks a device or dismisses the picker
Multi-deviceMany device.added events per scanOne pick per scan
scan.finishedWhen server finishesFires immediately after the picker closes
stopScanning()Cancels scanNo effect; picker is modal and already closed

Because scan.finished fires when the picker closes, device.added usually arrives after scan.finished (pairing and connection finish asynchronously). For UI code:

  • Label the scan button "Pair Device" rather than "Scan" when using WASM.
  • Don't show a "scanning…" spinner gated on scan.startedscan.finished; it will flicker.
  • Don't show "no devices found" between scan.finished and device.added; wait for device.added (with a timeout) instead.
  • Call startScanning() again to pair another device; it is one-pick-per-call.
client.on("scan.finished", () => {
  // Picker closed. Wait for device.added to know if pairing succeeded.
});

client.on("device.added", ({ data: { device } }) => {
  // Pairing complete; device is ready.
});

Browser requirements

Web Bluetooth requires a **secure context** (HTTPS or `localhost`) and a Chromium-based browser (Chrome, Edge, Opera). Firefox and Safari do not expose `navigator.bluetooth`; `connect()` fails with `ConnectionError`.

WasmTransport checks for navigator.bluetooth before loading WASM.

Limitations

TopicBehavior
Auto-reconnectNot supported (no network socket to restore)
DiscoveryBluetooth devices visible to the browser only
Bundle sizeWASM binary is large; lazy-load the /wasm entry
PatternsPatternEngine works the same once connected

Next steps