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.
- Connect: downloads
buttplug-wasm-blob, starts the in-process server, and completes the handshake. - Scan: opens the system Bluetooth picker to pair a device.
- 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 + Intiface | WASM + Web Bluetooth | |
|---|---|---|
| Runtime | Node, Bun, Deno, browsers | Chromium browsers |
| Server | Intiface Central (or other) | In-page WASM binary |
| Discovery | USB, serial, BLE relay, etc. | Browser Bluetooth picker only |
| Best for | Desktop tools, servers, broad hardware | Web-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) | |
|---|---|---|
| Trigger | Server actively scans backends | Browser opens device picker |
| Duration | Until stopScanning() or server timeout | Until user picks a device or dismisses the picker |
| Multi-device | Many device.added events per scan | One pick per scan |
scan.finished | When server finishes | Fires immediately after the picker closes |
stopScanning() | Cancels scan | No 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.started→scan.finished; it will flicker. - Don't show "no devices found" between
scan.finishedanddevice.added; wait fordevice.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
WasmTransport checks for navigator.bluetooth before loading WASM.
Limitations
| Topic | Behavior |
|---|---|
| Auto-reconnect | Not supported (no network socket to restore) |
| Discovery | Bluetooth devices visible to the browser only |
| Bundle size | WASM binary is large; lazy-load the /wasm entry |
| Patterns | PatternEngine works the same once connected |
Next steps
- Getting Started: WebSocket path with Intiface
- Connecting: shared
ButtplugClientoptions (exceptautoReconnect) - WasmTransport reference: full API