bugfix/socket-reconnetion #29
@ -8,6 +8,8 @@
 | 
			
		||||
// MIT License
 | 
			
		||||
 | 
			
		||||
import { Schedule } from './schedule.js';
 | 
			
		||||
import { EventHandler } from "./event-handler.js";
 | 
			
		||||
import { Socket } from "./socket.js";
 | 
			
		||||
 | 
			
		||||
export class RESTClient {
 | 
			
		||||
    debug = false;
 | 
			
		||||
@ -45,21 +47,6 @@ export class RESTClient {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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();
 | 
			
		||||
@ -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;
 | 
			
		||||
| 
					
	
	
	
	
	
	
	
	 | 
			||||
							
								
								
									
										16
									
								
								src/snek/static/event-handler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/snek/static/event-handler.js
									
									
									
									
									
										Normal file
									
								
							@ -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));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										137
									
								
								src/snek/static/socket.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								src/snek/static/socket.js
									
									
									
									
									
										Normal file
									
								
							@ -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));
 | 
			
		||||
| 
					
	
	
	
	
	
	
	
	 
				
					
						retoor
						commented  
			
		STill me vars, but out of scope . STill me vars, but out of scope . 
			
			
		 | 
			||||
            me.sendJson(call);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	
Because it's a module we no longer leak the app instance, this makes it easier to debug