|
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);
|
|
});
|
|
}
|
|
} |