Pokemon_server / server /loginserver.ts
Jofthomas's picture
Upload 4781 files
5c2ed06 verified
/**
* 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');
}