From 268d0f72a43b692b96241450d695d04ea482f7fb Mon Sep 17 00:00:00 2001 From: Aurastic <33085835+ISenseAura@users.noreply.github.com> Date: Mon, 26 May 2025 00:23:44 +0530 Subject: [PATCH] Preact: Support auto-reconnecting through web worker (#2409) --------- Co-authored-by: Guangcong Luo --- .gitignore | 1 + .../src/client-connection-worker.ts | 61 +++++++ .../src/client-connection.ts | 152 ++++++++++++++++-- play.pokemonshowdown.com/src/panel-chat.tsx | 4 +- .../src/panel-mainmenu.tsx | 4 +- 5 files changed, 207 insertions(+), 15 deletions(-) create mode 100644 play.pokemonshowdown.com/src/client-connection-worker.ts diff --git a/.gitignore b/.gitignore index 8350b8fd3..67b6b0ad4 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ npm-debug.log /play.pokemonshowdown.com/js/client-main.js /play.pokemonshowdown.com/js/client-core.js /play.pokemonshowdown.com/js/client-connection.js +/play.pokemonshowdown.com/js/client-connection-worker.js /play.pokemonshowdown.com/js/miniedit.js /play.pokemonshowdown.com/ads.txt diff --git a/play.pokemonshowdown.com/src/client-connection-worker.ts b/play.pokemonshowdown.com/src/client-connection-worker.ts new file mode 100644 index 000000000..3f6fe9814 --- /dev/null +++ b/play.pokemonshowdown.com/src/client-connection-worker.ts @@ -0,0 +1,61 @@ +declare const SockJS: any; +import type { ServerInfo } from "./client-main"; + +let socket: WebSocket | null = null; +let serverInfo: ServerInfo; +let reconnectTimeout: ReturnType | null = null; +let queue: string[] = []; + +self.onmessage = (event: MessageEvent) => { + const { type, server, data } = event.data; + if (type === 'connect') { + serverInfo = server; + connectToServer(); + } else if (type === 'send') { + if (socket && socket.readyState === WebSocket.OPEN) { + socket.send(data); + } else { + queue.push(data); + } + } else if (type === 'disconnect') { + if (socket) socket.close(); + if (reconnectTimeout) clearTimeout(reconnectTimeout); + socket = null; + } +}; + +function connectToServer() { + if (!serverInfo) return; + + const port = serverInfo.protocol === 'https' ? '' : `:${serverInfo.port}`; + const url = `${serverInfo.protocol}://${serverInfo.host}${port}${serverInfo.prefix}`; + + try { + socket = new SockJS(url, [], { timeout: 5 * 60 * 1000 }); + } catch { + socket = new WebSocket(url.replace('http', 'ws') + '/websocket'); + } + if (socket) { + socket.onopen = () => { + postMessage({ type: 'connected' }); + for (const msg of queue) socket?.send(msg); + queue = []; + }; + + socket.onmessage = (e: MessageEvent) => { + postMessage({ type: 'message', data: e.data }); + }; + + socket.onclose = () => { + postMessage({ type: 'disconnected' }); + // scheduleReconnect(); + }; + + socket.onerror = () => { + postMessage({ type: 'error' }); + socket?.close(); + }; + return; + } + return postMessage({ type: 'error' }); +} diff --git a/play.pokemonshowdown.com/src/client-connection.ts b/play.pokemonshowdown.com/src/client-connection.ts index 2c3016b63..8212d4193 100644 --- a/play.pokemonshowdown.com/src/client-connection.ts +++ b/play.pokemonshowdown.com/src/client-connection.ts @@ -13,39 +13,122 @@ declare const POKEMON_SHOWDOWN_TESTCLIENT_KEY: string | undefined; export class PSConnection { socket: any = null; connected = false; - queue = [] as string[]; + queue: string[] = []; + private reconnectDelay = 1000; + private reconnectCap = 15000; + private shouldReconnect = true; + private worker: Worker | null = null; + constructor() { const loading = PSStorage.init(); if (loading) { loading.then(() => { - this.connect(); + this.initConnection(); }); } else { - this.connect(); + this.initConnection(); } } - connect() { + + initConnection() { + if (!this.tryConnectInWorker()) this.directConnect(); + } + + canReconnect() { + const uptime = Date.now() - PS.startTime; + if (uptime > 24 * 60 * 60 * 1000) { + PS.confirm(`It's been over a day since you first connected. Please refresh.`, { + okButton: 'Refresh', + }).then(confirmed => { + if (confirmed) PS.room?.send(`/refresh`); + }); + return false; + } + return this.shouldReconnect; + } + + tryConnectInWorker(): boolean { + if (this.socket) return false; // must be one or the other + + try { + const worker = new Worker('/js/reconnect-worker.js'); + this.worker = worker; + + worker.postMessage({ type: 'connect', server: PS.server }); + + worker.onmessage = event => { + const { type, data } = event.data; + switch (type) { + case 'connected': + console.log('\u2705 (CONNECTED via worker)'); + this.connected = true; + PS.connected = true; + this.queue.forEach(msg => worker.postMessage({ type: 'send', data: msg })); + this.queue = []; + PS.update(); + break; + case 'message': + PS.receive(data); + break; + case 'disconnected': + this.handleDisconnect(); + if (this.canReconnect()) this.retryConnection(); + break; + case 'error': + console.warn('Worker connection error'); + this.worker = null; + this.directConnect(); // fallback + break; + } + }; + + worker.onerror = (e: ErrorEvent) => { + console.warn('Worker connection error:', e); + this.worker = null; + this.directConnect(); // fallback + }; + + return true; + } catch { + console.warn('Worker connection failed, falling back to regular connection.'); + this.worker = null; + return false; + } + } + + directConnect() { + if (this.worker) return; // must be one or the other + const server = PS.server; - const port = server.protocol === 'https' ? `:${server.port}` : `:${server.httpport || server.port}`; + const port = server.protocol === 'https' ? `:${server.port}` : `:${server.httpport!}`; const url = `${server.protocol}://${server.host}${port}${server.prefix}`; + try { this.socket = new SockJS(url, [], { timeout: 5 * 60 * 1000 }); } catch { this.socket = new WebSocket(url.replace('http', 'ws') + '/websocket'); } + const socket = this.socket; + socket.onopen = () => { console.log('\u2705 (CONNECTED)'); this.connected = true; PS.connected = true; - for (const msg of this.queue) socket.send(msg); + this.reconnectDelay = 1000; + this.queue.forEach(msg => socket.send(msg)); this.queue = []; PS.update(); }; + socket.onmessage = (e: MessageEvent) => { PS.receive('' + e.data); }; + socket.onclose = () => { + console.log('\u274C (DISCONNECTED)'); + this.handleDisconnect(); + if (this.canReconnect()) this.retryConnection(); console.log('\u2705 (DISCONNECTED)'); this.connected = false; PS.connected = false; @@ -57,32 +140,75 @@ export class PSConnection { this.socket = null; PS.update(); }; + socket.onerror = () => { PS.connected = false; PS.isOffline = true; PS.alert("Connection error."); + if (this.canReconnect()) this.retryConnection(); }; } + + private handleDisconnect() { + this.connected = false; + PS.connected = false; + PS.isOffline = true; + this.socket = null; + for (const roomid in PS.rooms) { + const room = PS.rooms[roomid]!; + if (room.connected === true) room.connected = 'autoreconnect'; + } + PS.update(); + } + + private retryConnection() { + console.log(`Reconnecting in ${this.reconnectDelay / 1000}s...`); + PS.room.add(`||You are disconnected. Attempting to reconnect in ${this.reconnectDelay / 1000}s`); + setTimeout(() => { + if (!this.connected && this.canReconnect()) { + PSConnection.connect(); // or PS.send('/reconnect') ? + this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.reconnectCap); + } + }, this.reconnectDelay); + } + disconnect() { - this.socket.close(); + this.shouldReconnect = false; + this.socket?.close(); + this.worker?.terminate(); + this.worker = null; PS.connection = null; PS.connected = false; PS.isOffline = true; } + + reconnectTest() { + this.socket?.close(); + this.worker?.postMessage({ type: 'disconnect' }); + this.worker = null; + PS.connected = false; + PS.isOffline = true; + } + send(msg: string) { if (!this.connected) { this.queue.push(msg); return; } - this.socket.send(msg); + if (this.worker) { + this.worker.postMessage({ type: 'send', data: msg }); + } else if (this.socket) { + this.socket.send(msg); + } } + static connect() { if (PS.connection?.socket) return; PS.isOffline = false; if (!PS.connection) { PS.connection = new PSConnection(); } else { - PS.connection.connect(); + PS.connection.directConnect(); } PS.prefs.doAutojoin(); } @@ -92,7 +218,7 @@ export class PSStorage { static frame: WindowProxy | null = null; static requests: Record void> | null = null; static requestCount = 0; - static readonly origin = 'https://' + Config.routes.client; + static readonly origin = `https://${Config.routes.client}`; static loader?: () => void; static loaded: Promise | boolean = false; static init(): void | Promise { @@ -102,7 +228,7 @@ export class PSStorage { } if (Config.testclient) { return; - } else if (location.protocol + '//' + location.hostname === PSStorage.origin) { + } else if (`${location.protocol}//${location.hostname}` === PSStorage.origin) { // Same origin, everything can be kept as default Config.server ||= Config.defaultserver; return; @@ -128,7 +254,7 @@ export class PSStorage { } else { Config.server ||= Config.defaultserver; $( - '' + `` ).appendTo('body'); setTimeout(() => { // HTTPS may be blocked @@ -154,7 +280,7 @@ export class PSStorage { if (Config.server.registered && Config.server.id !== 'showdown' && Config.server.id !== 'smogtours') { const link = document.createElement('link'); link.rel = 'stylesheet'; - link.href = '//' + Config.routes.client + '/customcss.php?server=' + encodeURIComponent(Config.server.id); + link.href = `//${Config.routes.client}/customcss.php?server=${encodeURIComponent(Config.server.id)}`; document.head.appendChild(link); } Object.assign(PS.server, Config.server); diff --git a/play.pokemonshowdown.com/src/panel-chat.tsx b/play.pokemonshowdown.com/src/panel-chat.tsx index e3341965f..9e9c53cc9 100644 --- a/play.pokemonshowdown.com/src/panel-chat.tsx +++ b/play.pokemonshowdown.com/src/panel-chat.tsx @@ -1169,7 +1169,9 @@ class ChatPanel extends PSRoomPanel { return {challengeTo}{challengeFrom}{PS.isOffline &&

- +

}
{room.tour && } diff --git a/play.pokemonshowdown.com/src/panel-mainmenu.tsx b/play.pokemonshowdown.com/src/panel-mainmenu.tsx index 5caacb920..867ec0b94 100644 --- a/play.pokemonshowdown.com/src/panel-mainmenu.tsx +++ b/play.pokemonshowdown.com/src/panel-mainmenu.tsx @@ -596,7 +596,9 @@ class MainMenuPanel extends PSRoomPanel { , " Disconnected"] : "Connecting..."} {PS.isOffline &&

- +

} ; }