|
// 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();
|