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);
this.emit("channel-message", data);
}
}
disconnect() {
this.ws?.close();
this.ws = null;
if (this.shouldReconnect) setTimeout(() => {
console.log("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);
});
}
}