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