mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-03-21 17:50:29 -05:00
Preact: Support auto-reconnecting through web worker (#2409)
--------- Co-authored-by: Guangcong Luo <guangcongluo@gmail.com>
This commit is contained in:
parent
1d5e1baa32
commit
268d0f72a4
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
61
play.pokemonshowdown.com/src/client-connection-worker.ts
Normal file
61
play.pokemonshowdown.com/src/client-connection-worker.ts
Normal file
|
|
@ -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<typeof setTimeout> | 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' });
|
||||
}
|
||||
|
|
@ -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<string, (data: any) => 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<void> | boolean = false;
|
||||
static init(): void | Promise<void> {
|
||||
|
|
@ -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;
|
||||
$(
|
||||
'<iframe src="https://' + Config.routes.client + '/crossprotocol.html?v1.2" style="display: none;"></iframe>'
|
||||
`<iframe src="https://${Config.routes.client}/crossprotocol.html?v1.2" style="display: none;"></iframe>`
|
||||
).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);
|
||||
|
|
|
|||
|
|
@ -1169,7 +1169,9 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
|
|||
return <PSPanelWrapper room={room} focusClick>
|
||||
<ChatLog class="chat-log" room={this.props.room} left={tinyLayout ? 0 : 146} top={room.tour?.info.isActive ? 30 : 0}>
|
||||
{challengeTo}{challengeFrom}{PS.isOffline && <p class="buttonbar">
|
||||
<button class="button" data-cmd="/reconnect"><i class="fa fa-plug" aria-hidden></i> <strong>Reconnect</strong></button>
|
||||
<button class="button" data-cmd="/reconnect">
|
||||
<i class="fa fa-plug" aria-hidden></i> <strong>Reconnect Now</strong>
|
||||
</button>
|
||||
</p>}
|
||||
</ChatLog>
|
||||
{room.tour && <TournamentBox tour={room.tour} left={tinyLayout ? 0 : 146} />}
|
||||
|
|
|
|||
|
|
@ -596,7 +596,9 @@ class MainMenuPanel extends PSRoomPanel<MainMenuRoom> {
|
|||
</span>, " Disconnected"] : "Connecting..."}</em>
|
||||
</button>
|
||||
{PS.isOffline && <p class="buttonbar">
|
||||
<button class="button" data-cmd="/reconnect"><i class="fa fa-plug" aria-hidden></i> <strong>Reconnect</strong></button>
|
||||
<button class="button" data-cmd="/reconnect">
|
||||
<i class="fa fa-plug" aria-hidden></i> <strong>Reconnect Now</strong>
|
||||
</button>
|
||||
</p>}
|
||||
</TeamForm>;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user