diff --git a/src/snek/static/app.js b/src/snek/static/app.js index 29aedb2..e4a59ea 100644 --- a/src/snek/static/app.js +++ b/src/snek/static/app.js @@ -7,7 +7,9 @@ // MIT License -import { Schedule } from './schedule.js'; +import {Schedule} from './schedule.js'; +import {EventHandler} from "./event-handler.js"; +import {Socket} from "./socket.js"; export class RESTClient { debug = false; @@ -23,7 +25,7 @@ export class RESTClient { }); const result = await response.json(); if (this.debug) { - console.debug({ url, params, result }); + console.debug({url, params, result}); } return result; } @@ -39,27 +41,12 @@ export class RESTClient { const result = await response.json(); if (this.debug) { - console.debug({ url, data, result }); + console.debug({url, data, result}); } return result; } } -export 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)); - } -} - export class Chat extends EventHandler { constructor() { super(); @@ -100,7 +87,7 @@ export class Chat extends EventHandler { call(method, ...args) { return new Promise((resolve, reject) => { try { - const command = { method, args, message_id: this.generateUniqueId() }; + const command = {method, args, message_id: this.generateUniqueId()}; this._promises[command.message_id] = resolve; this._socket.send(JSON.stringify(command)); } catch (e) { @@ -134,133 +121,6 @@ export class Chat extends EventHandler { } } -export 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) => { - if(me.isConnected)resolve(me) - else if(me.isConnecting) - me.connectPromises.push(resolve); - }); - } - this.isConnecting = true; - return new Promise((resolve) => { - this.connectPromises.push(resolve); - console.debug("Connecting.."); - - const ws = new WebSocket(this.url); - ws.onopen = () => { - this.ws = ws; - this.isConnected = true; - this.isConnecting = false; - ws.onmessage = (event) => { - this.onData(JSON.parse(event.data)); - }; - ws.onclose = () => { - this.onClose(); - }; - ws.onerror = () => { - this.onClose(); - }; - this.onConnect() - this.connectPromises.forEach(resolver => resolver(this)); - }; - }); - } - onConnect(){ - this.emit("connected") - } - 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.ws.close(); - this.ws = null; - this.ensureConnection().then(() => { - console.info("Reconnected."); - }); - } -} - export class NotificationAudio { constructor(timeout = 500) { this.schedule = new Schedule(timeout); @@ -268,9 +128,9 @@ export class NotificationAudio { sounds = { "message": "/audio/soundfx.d_beep3.mp3", - "mention": "/audio/750607__deadrobotmusic__notification-sound-1.wav", - "messageOtherChannel": "/audio/750608__deadrobotmusic__notification-sound-2.wav", - "ping": "/audio/750609__deadrobotmusic__notification-sound-3.wav", + "mention": "/audio/750607__deadrobotmusic__notification-sound-1.wav", + "messageOtherChannel": "/audio/750608__deadrobotmusic__notification-sound-2.wav", + "ping": "/audio/750609__deadrobotmusic__notification-sound-3.wav", } play(soundIndex = 0) { @@ -294,11 +154,12 @@ export class App extends EventHandler { user = {}; async ping(...args) { - if(this.is_pinging)return false + if (this.is_pinging) return false this.is_pinging = true await this.rpc.ping(...args); this.is_pinging = false } + async forcePing(...arg) { await this.rpc.ping(...args); } @@ -308,14 +169,14 @@ export class App extends EventHandler { this.ws = new Socket(); this.rpc = this.ws.client; this.audio = new NotificationAudio(500); - this.is_pinging = false - this.ping_interval = setInterval(()=>{ + this.is_pinging = false + this.ping_interval = setInterval(() => { this.ping("active") }, 15000) - - const me = this - this.ws.addEventListener("connected", (data)=> { + + const me = this + this.ws.addEventListener("connected", (data) => { this.ping("online") }) this.ws.addEventListener("channel-message", (data) => { @@ -330,6 +191,7 @@ export class App extends EventHandler { playSound(index) { this.audio.play(index); } + timeDescription(isoDate) { const date = new Date(isoDate); const hours = String(date.getHours()).padStart(2, "0"); @@ -337,6 +199,7 @@ export class App extends EventHandler { let timeStr = `${hours}:${minutes}, ${this.timeAgo(new Date(isoDate), Date.now())}`; return timeStr; } + timeAgo(date1, date2) { const diffMs = Math.abs(date2 - date1); const days = Math.floor(diffMs / (1000 * 60 * 60 * 24)); @@ -355,9 +218,10 @@ export class App extends EventHandler { } return 'just now'; } + async benchMark(times = 100, message = "Benchmark Message") { const promises = []; - const me = this; + const me = this; for (let i = 0; i < times; i++) { promises.push(this.rpc.getChannels().then(channels => { channels.forEach(channel => { @@ -369,3 +233,4 @@ export class App extends EventHandler { } export const app = new App(); +window.app = app; \ No newline at end of file diff --git a/src/snek/static/event-handler.js b/src/snek/static/event-handler.js new file mode 100644 index 0000000..a6d00e4 --- /dev/null +++ b/src/snek/static/event-handler.js @@ -0,0 +1,16 @@ + + +export 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)); + } +} \ No newline at end of file diff --git a/src/snek/static/socket.js b/src/snek/static/socket.js new file mode 100644 index 0000000..83f6cac --- /dev/null +++ b/src/snek/static/socket.js @@ -0,0 +1,137 @@ +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); + }); + } +} \ No newline at end of file