Merge pull request 'bugfix/socket-reconnetion' (#29) from BordedDev/snek:bugfix/socket-reconnetion into main
Reviewed-on: https://molodetz.nl/retoor/snek/pulls/29 Reviewed-by: retoor <retoor@noreply@molodetz.nl>
This commit is contained in:
		
						commit
						e62a855409
					
				| @ -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); | ||||
|     } | ||||
| @ -309,13 +170,13 @@ export class App extends EventHandler { | ||||
|         this.rpc = this.ws.client; | ||||
|         this.audio = new NotificationAudio(500); | ||||
|         this.is_pinging = false | ||||
|         this.ping_interval = setInterval(()=>{ | ||||
|         this.ping_interval = setInterval(() => { | ||||
|             this.ping("active") | ||||
|         }, 15000) | ||||
| 
 | ||||
| 
 | ||||
|         const me = this | ||||
|         this.ws.addEventListener("connected", (data)=> { | ||||
|         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,6 +218,7 @@ export class App extends EventHandler { | ||||
|         } | ||||
|         return 'just now'; | ||||
|     } | ||||
| 
 | ||||
|     async benchMark(times = 100, message = "Benchmark Message") { | ||||
|         const promises = []; | ||||
|         const me = this; | ||||
| @ -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)); | ||||
|             me.sendJson(call); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user