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-main.js
|
||||||
/play.pokemonshowdown.com/js/client-core.js
|
/play.pokemonshowdown.com/js/client-core.js
|
||||||
/play.pokemonshowdown.com/js/client-connection.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/js/miniedit.js
|
||||||
/play.pokemonshowdown.com/ads.txt
|
/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 {
|
export class PSConnection {
|
||||||
socket: any = null;
|
socket: any = null;
|
||||||
connected = false;
|
connected = false;
|
||||||
queue = [] as string[];
|
queue: string[] = [];
|
||||||
|
private reconnectDelay = 1000;
|
||||||
|
private reconnectCap = 15000;
|
||||||
|
private shouldReconnect = true;
|
||||||
|
private worker: Worker | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const loading = PSStorage.init();
|
const loading = PSStorage.init();
|
||||||
if (loading) {
|
if (loading) {
|
||||||
loading.then(() => {
|
loading.then(() => {
|
||||||
this.connect();
|
this.initConnection();
|
||||||
});
|
});
|
||||||
} else {
|
} 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 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}`;
|
const url = `${server.protocol}://${server.host}${port}${server.prefix}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.socket = new SockJS(url, [], { timeout: 5 * 60 * 1000 });
|
this.socket = new SockJS(url, [], { timeout: 5 * 60 * 1000 });
|
||||||
} catch {
|
} catch {
|
||||||
this.socket = new WebSocket(url.replace('http', 'ws') + '/websocket');
|
this.socket = new WebSocket(url.replace('http', 'ws') + '/websocket');
|
||||||
}
|
}
|
||||||
|
|
||||||
const socket = this.socket;
|
const socket = this.socket;
|
||||||
|
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
console.log('\u2705 (CONNECTED)');
|
console.log('\u2705 (CONNECTED)');
|
||||||
this.connected = true;
|
this.connected = true;
|
||||||
PS.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 = [];
|
this.queue = [];
|
||||||
PS.update();
|
PS.update();
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.onmessage = (e: MessageEvent) => {
|
socket.onmessage = (e: MessageEvent) => {
|
||||||
PS.receive('' + e.data);
|
PS.receive('' + e.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.onclose = () => {
|
socket.onclose = () => {
|
||||||
|
console.log('\u274C (DISCONNECTED)');
|
||||||
|
this.handleDisconnect();
|
||||||
|
if (this.canReconnect()) this.retryConnection();
|
||||||
console.log('\u2705 (DISCONNECTED)');
|
console.log('\u2705 (DISCONNECTED)');
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
PS.connected = false;
|
PS.connected = false;
|
||||||
|
|
@ -57,32 +140,75 @@ export class PSConnection {
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
PS.update();
|
PS.update();
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.onerror = () => {
|
socket.onerror = () => {
|
||||||
PS.connected = false;
|
PS.connected = false;
|
||||||
PS.isOffline = true;
|
PS.isOffline = true;
|
||||||
PS.alert("Connection error.");
|
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() {
|
disconnect() {
|
||||||
this.socket.close();
|
this.shouldReconnect = false;
|
||||||
|
this.socket?.close();
|
||||||
|
this.worker?.terminate();
|
||||||
|
this.worker = null;
|
||||||
PS.connection = null;
|
PS.connection = null;
|
||||||
PS.connected = false;
|
PS.connected = false;
|
||||||
PS.isOffline = true;
|
PS.isOffline = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reconnectTest() {
|
||||||
|
this.socket?.close();
|
||||||
|
this.worker?.postMessage({ type: 'disconnect' });
|
||||||
|
this.worker = null;
|
||||||
|
PS.connected = false;
|
||||||
|
PS.isOffline = true;
|
||||||
|
}
|
||||||
|
|
||||||
send(msg: string) {
|
send(msg: string) {
|
||||||
if (!this.connected) {
|
if (!this.connected) {
|
||||||
this.queue.push(msg);
|
this.queue.push(msg);
|
||||||
return;
|
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() {
|
static connect() {
|
||||||
if (PS.connection?.socket) return;
|
if (PS.connection?.socket) return;
|
||||||
PS.isOffline = false;
|
PS.isOffline = false;
|
||||||
if (!PS.connection) {
|
if (!PS.connection) {
|
||||||
PS.connection = new PSConnection();
|
PS.connection = new PSConnection();
|
||||||
} else {
|
} else {
|
||||||
PS.connection.connect();
|
PS.connection.directConnect();
|
||||||
}
|
}
|
||||||
PS.prefs.doAutojoin();
|
PS.prefs.doAutojoin();
|
||||||
}
|
}
|
||||||
|
|
@ -92,7 +218,7 @@ export class PSStorage {
|
||||||
static frame: WindowProxy | null = null;
|
static frame: WindowProxy | null = null;
|
||||||
static requests: Record<string, (data: any) => void> | null = null;
|
static requests: Record<string, (data: any) => void> | null = null;
|
||||||
static requestCount = 0;
|
static requestCount = 0;
|
||||||
static readonly origin = 'https://' + Config.routes.client;
|
static readonly origin = `https://${Config.routes.client}`;
|
||||||
static loader?: () => void;
|
static loader?: () => void;
|
||||||
static loaded: Promise<void> | boolean = false;
|
static loaded: Promise<void> | boolean = false;
|
||||||
static init(): void | Promise<void> {
|
static init(): void | Promise<void> {
|
||||||
|
|
@ -102,7 +228,7 @@ export class PSStorage {
|
||||||
}
|
}
|
||||||
if (Config.testclient) {
|
if (Config.testclient) {
|
||||||
return;
|
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
|
// Same origin, everything can be kept as default
|
||||||
Config.server ||= Config.defaultserver;
|
Config.server ||= Config.defaultserver;
|
||||||
return;
|
return;
|
||||||
|
|
@ -128,7 +254,7 @@ export class PSStorage {
|
||||||
} else {
|
} else {
|
||||||
Config.server ||= Config.defaultserver;
|
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');
|
).appendTo('body');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// HTTPS may be blocked
|
// HTTPS may be blocked
|
||||||
|
|
@ -154,7 +280,7 @@ export class PSStorage {
|
||||||
if (Config.server.registered && Config.server.id !== 'showdown' && Config.server.id !== 'smogtours') {
|
if (Config.server.registered && Config.server.id !== 'showdown' && Config.server.id !== 'smogtours') {
|
||||||
const link = document.createElement('link');
|
const link = document.createElement('link');
|
||||||
link.rel = 'stylesheet';
|
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);
|
document.head.appendChild(link);
|
||||||
}
|
}
|
||||||
Object.assign(PS.server, Config.server);
|
Object.assign(PS.server, Config.server);
|
||||||
|
|
|
||||||
|
|
@ -1169,7 +1169,9 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
|
||||||
return <PSPanelWrapper room={room} focusClick>
|
return <PSPanelWrapper room={room} focusClick>
|
||||||
<ChatLog class="chat-log" room={this.props.room} left={tinyLayout ? 0 : 146} top={room.tour?.info.isActive ? 30 : 0}>
|
<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">
|
{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>}
|
</p>}
|
||||||
</ChatLog>
|
</ChatLog>
|
||||||
{room.tour && <TournamentBox tour={room.tour} left={tinyLayout ? 0 : 146} />}
|
{room.tour && <TournamentBox tour={room.tour} left={tinyLayout ? 0 : 146} />}
|
||||||
|
|
|
||||||
|
|
@ -596,7 +596,9 @@ class MainMenuPanel extends PSRoomPanel<MainMenuRoom> {
|
||||||
</span>, " Disconnected"] : "Connecting..."}</em>
|
</span>, " Disconnected"] : "Connecting..."}</em>
|
||||||
</button>
|
</button>
|
||||||
{PS.isOffline && <p class="buttonbar">
|
{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>}
|
</p>}
|
||||||
</TeamForm>;
|
</TeamForm>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user