// Written by retoor@molodetz.nl
// This project implements a client-server communication system using WebSockets and REST APIs.
// It features a chat system, a notification sound system, and interaction with server endpoints.
// No additional imports were used beyond standard JavaScript objects and constructors.
// MIT License
class RESTClient {
debug = false;
async get(url, params = {}) {
const encodedParams = new URLSearchParams(params);
if (encodedParams) url += '?' + encodedParams;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const result = await response.json();
if (this.debug) {
console.debug({ url, params, result });
}
return result;
}
async post(url, data) {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
if (this.debug) {
console.debug({ url, data, result });
}
return result;
}
}
class EventHandler {
constructor() {
this.subscribers = {};
}
addEventListener(type, handler) {
if (!this.subscribers[type]) this.subscribers[type] = [];
this.subscribers[type].push(handler);
}
emit(type, ...data) {
if (this.subscribers[type]) this.subscribers[type].forEach(handler => handler(...data));
}
}
class Chat extends EventHandler {
constructor() {
super();
this._url = window.location.hostname === 'localhost' ? 'ws://localhost/chat.ws' : 'wss://' + window.location.hostname + '/chat.ws';
this._socket = null;
this._waitConnect = null;
this._promises = {};
}
connect() {
if (this._waitConnect) {
return this._waitConnect;
}
return new Promise((resolve) => {
this._waitConnect = resolve;
console.debug("Connecting..");
try {
this._socket = new WebSocket(this._url);
} catch (e) {
console.warn(e);
setTimeout(() => {
this.ensureConnection();
}, 1000);
}
this._socket.onconnect = () => {
this._connected();
this._waitSocket();
};
});
}
generateUniqueId() {
return 'id-' + Math.random().toString(36).substr(2, 9);
}
call(method, ...args) {
return new Promise((resolve, reject) => {
try {
const command = { method, args, message_id: this.generateUniqueId() };
this._promises[command.message_id] = resolve;
this._socket.send(JSON.stringify(command));
} catch (e) {
reject(e);
}
});
}
_connected() {
this._socket.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.message_id && this._promises[message.message_id]) {
this._promises[message.message_id](message);
delete this._promises[message.message_id];
} else {
this.emit("message", message);
}
};
this._socket.onclose = () => {
this._waitSocket = null;
this._socket = null;
this.emit('close');
};
}
async privmsg(room, text) {
await rest.post("/api/privmsg", {
room,
text,
});
}
}
class Socket extends EventHandler {
ws = null;
isConnected = null;
isConnecting = null;
url = null;
connectPromises = [];
ensureTimer = null;
constructor() {
super();
this.url = window.location.hostname === 'localhost' ? 'ws://localhost:8081/rpc.ws' : 'wss://' + window.location.hostname + '/rpc.ws';
this.ensureConnection();
}
_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);
};
},
});
}
ensureConnection() {
if (this.ensureTimer) {
return this.connect();
}
const me = this;
this.ensureTimer = setInterval(() => {
if (me.isConnecting) me.isConnecting = false;
me.connect();
}, 5000);
return this.connect();
}
generateUniqueId() {
return 'id-' + Math.random().toString(36).substr(2, 9);
}
connect() {
const me = this
if (this.isConnected || this.isConnecting) {
return new Promise((resolve) => {
me.connectPromises.push(resolve);
if (!me.isConnecting) resolve(me);
});
}
this.isConnecting = true;
return new Promise((resolve) => {
me.connectPromises.push(resolve);
console.debug("Connecting..");
const ws = new WebSocket(me.url);
ws.onopen = () => {
me.ws = ws;
me.isConnected = true;
me.isConnecting = false;
ws.onmessage = (event) => {
me.onData(JSON.parse(event.data));
};
ws.onclose = () => {
me.onClose();
};
ws.onerror = () => {
me.onClose();
};
me.connectPromises.forEach(resolver => resolver(me));
};
});
}
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);
}
}
async sendJson(data) {
await this.connect().then(api => {
api.ws.send(JSON.stringify(data));
});
}
async call(method, ...args) {
const call = {
callId: this.generateUniqueId(),
method,
args,
};
const me = this
return new Promise((resolve) => {
me.addEventListener(call.callId, data => resolve(data));
me.sendJson(call);
});
}
onClose() {
console.info("Connection lost. Reconnecting.");
this.isConnected = false;
this.isConnecting = false;
this.ensureConnection().then(() => {
console.info("Reconnected.");
});
}
}
class NotificationAudio {
constructor(timeout = 500) {
this.schedule = new Schedule(timeout);
}
sounds = ["/audio/soundfx.d_beep3.mp3"];
play(soundIndex = 0) {
this.schedule.delay(() => {
new Audio(this.sounds[soundIndex]).play()
.then(() => {
console.debug("Gave sound notification");
})
.catch(error => {
console.error("Notification failed:", error);
});
});
}
}
class App extends EventHandler {
rest = new RESTClient();
ws = null;
rpc = null;
audio = null;
user = {};
constructor() {
super();
this.ws = new Socket();
this.rpc = this.ws.client;
this.audio = new NotificationAudio(500);
const me = this
this.ws.addEventListener("channel-message", (data) => {
me.emit(data.channel_uid, data);
});
this.rpc.getUser(null).then(user => {
me.user = user;
});
}
playSound(index) {
this.audio.play(index);
}
async benchMark(times = 100, message = "Benchmark Message") {
const promises = [];
const me = this;
for (let i = 0; i < times; i++) {
promises.push(this.rpc.getChannels().then(channels => {
channels.forEach(channel => {
me.rpc.sendMessage(channel.uid, `${message} ${i}`);
});
}));
}
}
}
const app = new App();