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
 | // 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 { | export class RESTClient { | ||||||
|     debug = false; |     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 { | export class Chat extends EventHandler { | ||||||
|     constructor() { |     constructor() { | ||||||
|         super(); |         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 { | export class NotificationAudio { | ||||||
|     constructor(timeout = 500) { |     constructor(timeout = 500) { | ||||||
|         this.schedule = new Schedule(timeout); |         this.schedule = new Schedule(timeout); | ||||||
| @ -268,9 +128,9 @@ export class NotificationAudio { | |||||||
| 
 | 
 | ||||||
|     sounds = { |     sounds = { | ||||||
|         "message": "/audio/soundfx.d_beep3.mp3", |         "message": "/audio/soundfx.d_beep3.mp3", | ||||||
|         "mention":  "/audio/750607__deadrobotmusic__notification-sound-1.wav", |         "mention": "/audio/750607__deadrobotmusic__notification-sound-1.wav", | ||||||
|         "messageOtherChannel":  "/audio/750608__deadrobotmusic__notification-sound-2.wav", |         "messageOtherChannel": "/audio/750608__deadrobotmusic__notification-sound-2.wav", | ||||||
|         "ping":  "/audio/750609__deadrobotmusic__notification-sound-3.wav", |         "ping": "/audio/750609__deadrobotmusic__notification-sound-3.wav", | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     play(soundIndex = 0) { |     play(soundIndex = 0) { | ||||||
| @ -294,11 +154,12 @@ export class App extends EventHandler { | |||||||
|     user = {}; |     user = {}; | ||||||
| 
 | 
 | ||||||
|     async ping(...args) { |     async ping(...args) { | ||||||
|         if(this.is_pinging)return false  |         if (this.is_pinging) return false | ||||||
|         this.is_pinging = true |         this.is_pinging = true | ||||||
|         await this.rpc.ping(...args); |         await this.rpc.ping(...args); | ||||||
|         this.is_pinging = false |         this.is_pinging = false | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     async forcePing(...arg) { |     async forcePing(...arg) { | ||||||
|         await this.rpc.ping(...args); |         await this.rpc.ping(...args); | ||||||
|     } |     } | ||||||
| @ -308,14 +169,14 @@ export class App extends EventHandler { | |||||||
|         this.ws = new Socket(); |         this.ws = new Socket(); | ||||||
|         this.rpc = this.ws.client; |         this.rpc = this.ws.client; | ||||||
|         this.audio = new NotificationAudio(500); |         this.audio = new NotificationAudio(500); | ||||||
|         this.is_pinging = false  |         this.is_pinging = false | ||||||
|         this.ping_interval = setInterval(()=>{ |         this.ping_interval = setInterval(() => { | ||||||
|             this.ping("active") |             this.ping("active") | ||||||
|         }, 15000) |         }, 15000) | ||||||
|          |  | ||||||
| 
 | 
 | ||||||
|         const me = this  | 
 | ||||||
|         this.ws.addEventListener("connected", (data)=> { |         const me = this | ||||||
|  |         this.ws.addEventListener("connected", (data) => { | ||||||
|             this.ping("online") |             this.ping("online") | ||||||
|         }) |         }) | ||||||
|         this.ws.addEventListener("channel-message", (data) => { |         this.ws.addEventListener("channel-message", (data) => { | ||||||
| @ -330,6 +191,7 @@ export class App extends EventHandler { | |||||||
|     playSound(index) { |     playSound(index) { | ||||||
|         this.audio.play(index); |         this.audio.play(index); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     timeDescription(isoDate) { |     timeDescription(isoDate) { | ||||||
|         const date = new Date(isoDate); |         const date = new Date(isoDate); | ||||||
|         const hours = String(date.getHours()).padStart(2, "0"); |         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())}`; |         let timeStr = `${hours}:${minutes}, ${this.timeAgo(new Date(isoDate), Date.now())}`; | ||||||
|         return timeStr; |         return timeStr; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     timeAgo(date1, date2) { |     timeAgo(date1, date2) { | ||||||
|         const diffMs = Math.abs(date2 - date1); |         const diffMs = Math.abs(date2 - date1); | ||||||
|         const days = Math.floor(diffMs / (1000 * 60 * 60 * 24)); |         const days = Math.floor(diffMs / (1000 * 60 * 60 * 24)); | ||||||
| @ -355,9 +218,10 @@ export class App extends EventHandler { | |||||||
|         } |         } | ||||||
|         return 'just now'; |         return 'just now'; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     async benchMark(times = 100, message = "Benchmark Message") { |     async benchMark(times = 100, message = "Benchmark Message") { | ||||||
|         const promises = []; |         const promises = []; | ||||||
|         const me = this;  |         const me = this; | ||||||
|         for (let i = 0; i < times; i++) { |         for (let i = 0; i < times; i++) { | ||||||
|             promises.push(this.rpc.getChannels().then(channels => { |             promises.push(this.rpc.getChannels().then(channels => { | ||||||
|                 channels.forEach(channel => { |                 channels.forEach(channel => { | ||||||
| @ -369,3 +233,4 @@ export class App extends EventHandler { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const app = new App(); | 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