|
import { EventHandler } from "./event-handler.js";
|
|
|
|
export class Socket extends EventHandler {
|
|
/**
|
|
* @type {URL}
|
|
*/
|
|
url;
|
|
/**
|
|
* @type {WebSocket|null}
|
|
*/
|
|
ws = null;
|
|
|
|
/**
|
|
* @type {null|PromiseWithResolvers<Socket>&{resolved?:boolean}}
|
|
*/
|
|
connection = null;
|
|
|
|
shouldReconnect = true;
|
|
|
|
get isConnected() {
|
|
return this.ws && this.ws.readyState === WebSocket.OPEN;
|
|
}
|
|
|
|
get isConnecting() {
|
|
return this.ws && this.ws.readyState === WebSocket.CONNECTING;
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.url = new URL("/rpc.ws", window.location.origin);
|
|
this.url.protocol = this.url.protocol.replace("http", "ws");
|
|
|
|
this.connect();
|
|
}
|
|
|
|
connect() {
|
|
if (this.ws) {
|
|
return this.connection.promise;
|
|
}
|
|
|
|
if (!this.connection || this.connection.resolved) {
|
|
this.connection = Promise.withResolvers();
|
|
}
|
|
|
|
this.ws = new WebSocket(this.url);
|
|
this.ws.addEventListener("open", () => {
|
|
this.connection.resolved = true;
|
|
this.connection.resolve(this);
|
|
this.emit("connected");
|
|
});
|
|
|
|
this.ws.addEventListener("close", () => {
|
|
console.log("Connection closed");
|
|
this.disconnect();
|
|
});
|
|
this.ws.addEventListener("error", (e) => {
|
|
console.error("Connection error", e);
|
|
this.disconnect();
|
|
});
|
|
this.ws.addEventListener("message", (e) => {
|
|
if (e.data instanceof Blob || e.data instanceof ArrayBuffer) {
|
|
console.error("Binary data not supported");
|
|
} else {
|
|
try {
|
|
this.onData(JSON.parse(e.data));
|
|
} catch (e) {
|
|
console.error("Failed to parse message", e);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
onData(data) {
|
|
if (data.success !== undefined && !data.success) {
|
|
console.error(data);
|
|
}
|
|
if (data.callId) {
|
|
this.emit(data.callId, data.data);
|
|
}
|
|
if (data.channel_uid) {
|
|
this.emit(data.channel_uid, data.data);
|
|
if (!data["event"]) this.emit("channel-message", data);
|
|
}
|
|
this.emit("data", data.data);
|
|
if (data["event"]) {
|
|
this.emit(data.event, data.data);
|
|
}
|
|
}
|
|
|
|
disconnect() {
|
|
this.ws?.close();
|
|
this.ws = null;
|
|
|
|
if (this.shouldReconnect)
|
|
setTimeout(() => {
|
|
console.log("Reconnecting");
|
|
this.emit("reconnecting");
|
|
return this.connect();
|
|
}, 0);
|
|
}
|
|
|
|
_camelToSnake(str) {
|
|
return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
|
}
|
|
|
|
get client() {
|
|
const me = this;
|
|
return new Proxy(
|
|
{},
|
|
{
|
|
get(_, prop) {
|
|
return (...args) => {
|
|
const functionName = me._camelToSnake(prop);
|
|
return me.call(functionName, ...args);
|
|
};
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
generateCallId() {
|
|
return self.crypto.randomUUID();
|
|
}
|
|
|
|
async sendJson(data) {
|
|
await this.connect().then((api) => {
|
|
api.ws.send(JSON.stringify(data));
|
|
});
|
|
}
|
|
|
|
async call(method, ...args) {
|
|
const call = {
|
|
callId: this.generateCallId(),
|
|
method,
|
|
args,
|
|
};
|
|
const me = this;
|
|
return new Promise((resolve) => {
|
|
me.addEventListener(call.callId, (data) => resolve(data));
|
|
me.sendJson(call);
|
|
});
|
|
}
|
|
}
|