Spaces:
Paused
Paused
| /** | |
| * 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<LoginServerResponse> { | |
| 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<LoginServerResponse> { | |
| 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'); | |
| } | |