/** * Login server abstraction layer * Pokemon Showdown - http://pokemonshowdown.com/ * * This file handles communicating with the login server. * * @license MIT */ const LOGIN_SERVER_TIMEOUT = 30000; const LOGIN_SERVER_BATCH_TIME = 1000; import { Net, FS } from '../lib'; /** * A custom error type used when requests to the login server take too long. */ class TimeoutError extends Error {} TimeoutError.prototype.name = TimeoutError.name; function parseJSON(json: string) { if (json.startsWith(']')) json = json.substr(1); const data: { error: string | null, json: any[] | null } = { error: null, json: null }; try { data.json = JSON.parse(json); } catch (err: any) { data.error = err.message; } return data; } type LoginServerResponse = [AnyObject, null] | [null, Error]; class LoginServerInstance { readonly uri: string; requestQueue: [AnyObject, (val: LoginServerResponse) => void][]; requestTimer: NodeJS.Timeout | null; requestLog: string; lastRequest: number; openRequests: number; disabled: false; [key: `${string}Server`]: LoginServerInstance | undefined; constructor() { this.uri = Config.loginserver; this.requestQueue = []; this.requestTimer = null; this.requestLog = ''; this.lastRequest = 0; this.openRequests = 0; this.disabled = false; } async instantRequest(action: string, data: AnyObject | null = null): Promise { if (this.openRequests > 5) { return Promise.resolve( [null, new RangeError("Request overflow")] ); } this.openRequests++; try { const request = Net(this.uri); const buffer = await request.get({ query: { ...data, act: action, serverid: Config.serverid, servertoken: Config.servertoken, nocache: new Date().getTime(), }, }); const json = parseJSON(buffer); this.openRequests--; if (json.error) { return [null, new Error(json.error)]; } this.openRequests--; return [json.json!, null]; } catch (error: any) { this.openRequests--; return [null, error]; } } request(action: string, data: AnyObject | null = null): Promise { if (this.disabled) { return Promise.resolve( [null, new Error(`Login server connection disabled.`)] ); } // ladderupdate and mmr are the most common actions // prepreplay is also common if (this[`${action}Server`]) { return this[`${action}Server`]!.request(action, data); } const actionData = data || {}; actionData.act = action; return new Promise(resolve => { this.requestQueue.push([actionData, resolve]); this.requestTimerPoke(); }); } requestTimerPoke() { // "poke" the request timer, i.e. make sure it knows it should make // a request soon // if we already have it going or the request queue is empty no need to do anything if (this.openRequests || this.requestTimer || !this.requestQueue.length) return; this.requestTimer = setTimeout(() => void this.makeRequests(), LOGIN_SERVER_BATCH_TIME); } async makeRequests() { this.requestTimer = null; const requests = this.requestQueue; this.requestQueue = []; if (!requests.length) return; const resolvers: ((val: LoginServerResponse) => void)[] = []; const dataList = []; for (const [data, resolve] of requests) { resolvers.push(resolve); dataList.push(data); } this.requestStart(requests.length); try { const request = Net(`${this.uri}action.php`); let buffer = await request.post({ body: { serverid: Config.serverid, servertoken: Config.servertoken, nocache: new Date().getTime(), json: JSON.stringify(dataList), }, timeout: LOGIN_SERVER_TIMEOUT, }); // console.log('RESPONSE: ' + buffer); const data = parseJSON(buffer).json; if (buffer.startsWith(`[{"actionsuccess":true,`)) { buffer = 'stream interrupt'; } if (!data) { if (buffer.includes('<')) buffer = 'invalid response'; throw new Error(buffer); } for (const [i, resolve] of resolvers.entries()) { resolve([data[i], null]); } this.requestEnd(); } catch (error: any) { for (const resolve of resolvers) { resolve([null, error]); } this.requestEnd(error); } } requestStart(size: number) { this.lastRequest = Date.now(); this.requestLog += ` | ${size} rqs: `; this.openRequests++; } requestEnd(error?: Error) { this.openRequests = 0; if (error && error instanceof TimeoutError) { this.requestLog += 'TIMEOUT'; } else { this.requestLog += `${(Date.now() - this.lastRequest) / 1000}s`; } this.requestLog = this.requestLog.substr(-1000); this.requestTimerPoke(); } getLog() { if (!this.lastRequest) return this.requestLog; return `${this.requestLog} (${Chat.toDurationString(Date.now() - this.lastRequest)} since last request)`; } } export const LoginServer = Object.assign(new LoginServerInstance(), { TimeoutError, ladderupdateServer: new LoginServerInstance(), prepreplayServer: new LoginServerInstance(), }); FS('./config/custom.css').onModify(() => { void LoginServer.request('invalidatecss'); }); if (!Config.nofswriting) { void LoginServer.request('invalidatecss'); }