feat: upgrade buttplug package to protocol v4 and WASM v10
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 7m30s

Upgrade the buttplug TypeScript client from class-based v3 protocol to
interface-based v4 protocol, and the Rust/WASM server from the monolithic
buttplug 9.0.9 crate to the split buttplug_core/buttplug_server/
buttplug_server_device_config 10.0.0 crates.

TypeScript changes:
- Messages are now plain interfaces with msgId()/setMsgId() helpers
- ActuatorType → OutputType, SensorType → InputType
- ScalarCmd/RotateCmd/LinearCmd → OutputCmd, SensorReadCmd → InputCmd
- Client.ts → ButtplugClient.ts, new DeviceCommand/DeviceFeature files
- Devices getter returns Map instead of array
- Removed class-transformer/reflect-metadata dependencies

Rust/WASM changes:
- Split imports across buttplug_core, buttplug_server, buttplug_server_device_config
- Removed ButtplugServerDowngradeWrapper (use ButtplugServer directly)
- Replaced ButtplugFuture/ButtplugFutureStateShared with tokio::sync::oneshot
- Updated Hardware::new for new 6-arg signature
- Uses git fork (valknarthing/buttplug) to fix missing wasm deps in buttplug_core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-06 14:46:47 +01:00
parent fed2dd65e5
commit 6ea4ed1933
31 changed files with 1763 additions and 2441 deletions

View File

@@ -6,86 +6,83 @@
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
*/
"use strict";
'use strict';
import { EventEmitter } from "eventemitter3";
import { ButtplugMessage } from "../core/Messages";
import { fromJSON } from "../core/MessageUtils";
import { EventEmitter } from 'eventemitter3';
import { ButtplugMessage } from '../core/Messages';
export class ButtplugBrowserWebsocketConnector extends EventEmitter {
protected _ws: WebSocket | undefined;
protected _websocketConstructor: typeof WebSocket | null = null;
protected _ws: WebSocket | undefined;
protected _websocketConstructor: typeof WebSocket | null = null;
public constructor(private _url: string) {
super();
}
public constructor(private _url: string) {
super();
}
public get Connected(): boolean {
return this._ws !== undefined;
}
public get Connected(): boolean {
return this._ws !== undefined;
}
public connect = async (): Promise<void> => {
return new Promise<void>((resolve, reject) => {
const ws = new (this._websocketConstructor ?? WebSocket)(this._url);
const onErrorCallback = (event: Event) => {
reject(event);
};
const onCloseCallback = (event: CloseEvent) => reject(event.reason);
ws.addEventListener("open", async () => {
this._ws = ws;
try {
await this.initialize();
this._ws.addEventListener("message", (msg) => {
this.parseIncomingMessage(msg);
});
this._ws.removeEventListener("close", onCloseCallback);
this._ws.removeEventListener("error", onErrorCallback);
this._ws.addEventListener("close", this.disconnect);
resolve();
} catch (e) {
reject(e);
}
});
// In websockets, our error rarely tells us much, as for security reasons
// browsers usually only throw Error Code 1006. It's up to those using this
// library to state what the problem might be.
public connect = async (): Promise<void> => {
return new Promise<void>((resolve, reject) => {
const ws = new (this._websocketConstructor ?? WebSocket)(this._url);
const onErrorCallback = (event: Event) => {reject(event)}
const onCloseCallback = (event: CloseEvent) => reject(event.reason)
ws.addEventListener('open', async () => {
this._ws = ws;
try {
await this.initialize();
this._ws.addEventListener('message', (msg) => {
this.parseIncomingMessage(msg);
});
this._ws.removeEventListener('close', onCloseCallback);
this._ws.removeEventListener('error', onErrorCallback);
this._ws.addEventListener('close', this.disconnect);
resolve();
} catch (e) {
reject(e);
}
});
// In websockets, our error rarely tells us much, as for security reasons
// browsers usually only throw Error Code 1006. It's up to those using this
// library to state what the problem might be.
ws.addEventListener("error", onErrorCallback);
ws.addEventListener("close", onCloseCallback);
});
};
ws.addEventListener('error', onErrorCallback)
ws.addEventListener('close', onCloseCallback);
});
};
public disconnect = async (): Promise<void> => {
if (!this.Connected) {
return;
}
this._ws!.close();
this._ws = undefined;
this.emit("disconnect");
};
public disconnect = async (): Promise<void> => {
if (!this.Connected) {
return;
}
this._ws!.close();
this._ws = undefined;
this.emit('disconnect');
};
public sendMessage(msg: ButtplugMessage) {
if (!this.Connected) {
throw new Error("ButtplugBrowserWebsocketConnector not connected");
}
this._ws!.send("[" + msg.toJSON() + "]");
}
public sendMessage(msg: ButtplugMessage) {
if (!this.Connected) {
throw new Error('ButtplugBrowserWebsocketConnector not connected');
}
this._ws!.send('[' + JSON.stringify(msg) + ']');
}
public initialize = async (): Promise<void> => {
return Promise.resolve();
};
public initialize = async (): Promise<void> => {
return Promise.resolve();
};
protected parseIncomingMessage(event: MessageEvent) {
if (typeof event.data === "string") {
const msgs = fromJSON(event.data);
this.emit("message", msgs);
} else if (event.data instanceof Blob) {
// No-op, we only use text message types.
}
}
protected parseIncomingMessage(event: MessageEvent) {
if (typeof event.data === 'string') {
const msgs: ButtplugMessage[] = JSON.parse(event.data);
this.emit('message', msgs);
} else if (event.data instanceof Blob) {
// No-op, we only use text message types.
}
}
protected onReaderLoad(event: Event) {
const msgs = fromJSON((event.target as FileReader).result);
this.emit("message", msgs);
}
protected onReaderLoad(event: Event) {
const msgs: ButtplugMessage[] = JSON.parse((event.target as FileReader).result as string);
this.emit('message', msgs);
}
}