@foxglove/ws-protocol-examples
Version:
Foxglove WebSocket protocol examples
180 lines • 6.94 kB
JavaScript
import EventEmitter from "eventemitter3";
import { parseServerMessage } from "./parse";
import { BinaryOpcode, ClientBinaryOpcode, } from "./types";
const textEncoder = new TextEncoder();
/**
* A client to interact with the Foxglove WebSocket protocol:
* https://github.com/foxglove/ws-protocol/blob/main/docs/spec.md.
*
* You must provide the underlying websocket client (an implementation of `IWebSocket`) and that
* client must advertise a subprotocol which is compatible with the ws-protocol spec (e.g.
* "foxglove.websocket.v1").
*/
export default class FoxgloveClient {
static SUPPORTED_SUBPROTOCOL = "foxglove.websocket.v1";
#emitter = new EventEmitter();
#ws;
#nextSubscriptionId = 0;
#nextAdvertisementId = 0;
constructor({ ws }) {
this.#ws = ws;
this.#reconnect();
}
on(name, listener) {
this.#emitter.on(name, listener);
}
off(name, listener) {
this.#emitter.off(name, listener);
}
#reconnect() {
this.#ws.binaryType = "arraybuffer";
this.#ws.onerror = (event) => {
this.#emitter.emit("error", event.error ?? new Error("WebSocket error"));
};
this.#ws.onopen = (_event) => {
this.#emitter.emit("open");
};
this.#ws.onmessage = (event) => {
let message;
try {
if (event.data instanceof ArrayBuffer || ArrayBuffer.isView(event.data)) {
message = parseServerMessage(event.data);
}
else {
message = JSON.parse(event.data);
}
}
catch (error) {
this.#emitter.emit("error", error);
return;
}
switch (message.op) {
case "serverInfo":
this.#emitter.emit("serverInfo", message);
return;
case "status":
this.#emitter.emit("status", message);
return;
case "removeStatus":
this.#emitter.emit("removeStatus", message);
return;
case "advertise":
this.#emitter.emit("advertise", message.channels);
return;
case "unadvertise":
this.#emitter.emit("unadvertise", message.channelIds);
return;
case "parameterValues":
this.#emitter.emit("parameterValues", message);
return;
case "advertiseServices":
this.#emitter.emit("advertiseServices", message.services);
return;
case "unadvertiseServices":
this.#emitter.emit("unadvertiseServices", message.serviceIds);
return;
case "connectionGraphUpdate":
this.#emitter.emit("connectionGraphUpdate", message);
return;
case "serviceCallFailure":
this.#emitter.emit("serviceCallFailure", message);
return;
case BinaryOpcode.MESSAGE_DATA:
this.#emitter.emit("message", message);
return;
case BinaryOpcode.TIME:
this.#emitter.emit("time", message);
return;
case BinaryOpcode.SERVICE_CALL_RESPONSE:
this.#emitter.emit("serviceCallResponse", message);
return;
case BinaryOpcode.FETCH_ASSET_RESPONSE:
this.#emitter.emit("fetchAssetResponse", message);
return;
}
this.#emitter.emit("error", new Error(`Unrecognized server opcode: ${message.op}`));
};
this.#ws.onclose = (event) => {
this.#emitter.emit("close", event);
};
}
close() {
this.#ws.close();
}
subscribe(channelId) {
const id = this.#nextSubscriptionId++;
const subscriptions = [{ id, channelId }];
this.#send({ op: "subscribe", subscriptions });
return id;
}
unsubscribe(subscriptionId) {
this.#send({ op: "unsubscribe", subscriptionIds: [subscriptionId] });
}
advertise(clientChannel) {
const id = ++this.#nextAdvertisementId;
const channels = [{ id, ...clientChannel }];
this.#send({ op: "advertise", channels });
return id;
}
unadvertise(channelId) {
this.#send({ op: "unadvertise", channelIds: [channelId] });
}
getParameters(parameterNames, id) {
this.#send({ op: "getParameters", parameterNames, id });
}
setParameters(parameters, id) {
this.#send({ op: "setParameters", parameters, id });
}
subscribeParameterUpdates(parameterNames) {
this.#send({ op: "subscribeParameterUpdates", parameterNames });
}
unsubscribeParameterUpdates(parameterNames) {
this.#send({ op: "unsubscribeParameterUpdates", parameterNames });
}
sendMessage(channelId, data) {
const payload = new Uint8Array(5 + data.byteLength);
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
view.setUint8(0, ClientBinaryOpcode.MESSAGE_DATA);
view.setUint32(1, channelId, true);
payload.set(data, 5);
this.#ws.send(payload);
}
sendServiceCallRequest(request) {
const encoding = textEncoder.encode(request.encoding);
const payload = new Uint8Array(1 + 4 + 4 + 4 + encoding.length + request.data.byteLength);
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
let offset = 0;
view.setUint8(offset, ClientBinaryOpcode.SERVICE_CALL_REQUEST);
offset += 1;
view.setUint32(offset, request.serviceId, true);
offset += 4;
view.setUint32(offset, request.callId, true);
offset += 4;
view.setUint32(offset, request.encoding.length, true);
offset += 4;
payload.set(encoding, offset);
offset += encoding.length;
const data = new Uint8Array(request.data.buffer, request.data.byteOffset, request.data.byteLength);
payload.set(data, offset);
this.#ws.send(payload);
}
subscribeConnectionGraph() {
this.#send({ op: "subscribeConnectionGraph" });
}
unsubscribeConnectionGraph() {
this.#send({ op: "unsubscribeConnectionGraph" });
}
fetchAsset(uri, requestId) {
this.#send({ op: "fetchAsset", uri, requestId });
}
/**
* @deprecated Use `sendServiceCallRequest` instead
*/
sendCallServiceRequest(request) {
this.sendServiceCallRequest(request);
}
#send(message) {
this.#ws.send(JSON.stringify(message));
}
}
//# sourceMappingURL=FoxgloveClient.js.map