att-client
Advanced tools
Comparing version 0.2.23 to 0.2.24
@@ -32,5 +32,8 @@ import type { ServerInfo } from '../Api'; | ||
disconnect(): void; | ||
/** | ||
* Reconnects to this server's console connection after the configured delay. | ||
*/ | ||
private reconnect; | ||
/** | ||
* Updates a server with new information. | ||
* Updates this server with new information. | ||
*/ | ||
@@ -37,0 +40,0 @@ update(status: ServerInfo): void; |
@@ -40,37 +40,47 @@ "use strict"; | ||
} | ||
this.status = 'connecting'; | ||
const that = this; | ||
let connection; | ||
function handleError(error) { | ||
that.logger.error(`Error on console connection on server ${that.id} (${that.name}).`, error.message); | ||
} | ||
function handleOpen() { | ||
that.logger.info(`Console connection opened on server ${that.id} (${that.name}).`); | ||
that.status = 'connected'; | ||
that.group.client.emit('connect', connection); | ||
} | ||
async function handleClose(code, reason) { | ||
connection.off('open', handleOpen); | ||
/* Reconnect console connection when closed unexpectedly. */ | ||
if (code === 3000) { | ||
that.disconnect(); | ||
} | ||
else { | ||
that.logger.info(`Console connection closed on server ${that.id} (${that.name}).`, code, reason?.toString()); | ||
await that.reconnect(); | ||
} | ||
} | ||
try { | ||
connection = new ServerConnection_1.ServerConnection(this, address, port, token); | ||
connection.on('error', handleError); | ||
connection.once('close', handleClose); | ||
connection.once('open', handleOpen); | ||
this.connection = connection; | ||
await new Promise((resolve, reject) => { | ||
function handleError(error) { | ||
that.logger.error(`Error on console connection on server ${that.id} (${that.name}).`, error.message); | ||
/** | ||
* If errors happen before the WebSocket connection is opened, it's likely | ||
* that the WebSocket won't open anymore. In this case we want to reject | ||
* so that we can trigger a recovery. | ||
* | ||
* In other cases, errors will happen during a connection. If that results | ||
* in the WebSocket closing, we can handle this in the onClose handler, | ||
* because supposedly not all errors result in a disconnect. | ||
*/ | ||
if (that.status !== 'connected') { | ||
reject(error); | ||
} | ||
} | ||
function handleOpen() { | ||
that.logger.info(`Console connection opened on server ${that.id} (${that.name}).`); | ||
that.status = 'connected'; | ||
that.group.client.emit('connect', this); | ||
resolve(); | ||
} | ||
async function handleClose(code, reason) { | ||
if (code === 1000) { | ||
that.disconnect(); | ||
} | ||
else { | ||
/* Reconnect console connection when closed unexpectedly. */ | ||
that.logger.info(`Console connection closed on server ${that.id} (${that.name}).`, code, reason?.toString()); | ||
await that.reconnect(); | ||
} | ||
} | ||
this.status = 'connecting'; | ||
const connection = new ServerConnection_1.ServerConnection(this, address, port, token); | ||
connection.on('error', handleError.bind(connection)); | ||
connection.once('close', handleClose.bind(connection)); | ||
connection.once('open', handleOpen.bind(connection)); | ||
this.connection = connection; | ||
}); | ||
} | ||
catch (error) { | ||
delete this.connection; | ||
this.status = 'disconnected'; | ||
this.logger.error(`Something went wrong opening a console connection to server ${this.name}. Retrying in ${this.group.client.config.serverConnectionRecoveryDelay} ms. Error was: ${error.message}`); | ||
await new Promise(resolve => setTimeout(resolve, this.group.client.config.serverConnectionRecoveryDelay)); | ||
await this.connect(); | ||
this.logger.error(`Something went wrong opening a console connection to server ${this.name}: ${error.message}`); | ||
await this.reconnect(); | ||
} | ||
@@ -89,2 +99,5 @@ } | ||
} | ||
/** | ||
* Reconnects to this server's console connection after the configured delay. | ||
*/ | ||
async reconnect() { | ||
@@ -99,3 +112,3 @@ if (typeof this.connection === 'undefined') | ||
/** | ||
* Updates a server with new information. | ||
* Updates this server with new information. | ||
*/ | ||
@@ -102,0 +115,0 @@ update(status) { |
@@ -59,3 +59,2 @@ "use strict"; | ||
that.logger.debug(`Registering console ${that.server.id} (${that.server.name}) event handlers.`); | ||
this.on('error', handleError); | ||
this.on('ping', handlePing); | ||
@@ -73,2 +72,3 @@ this.on('pong', handlePong); | ||
this.ws = new ws_1.WebSocket(`ws://${address}:${port}`); | ||
this.ws.on('error', handleError); | ||
this.ws.once('close', handleClose); | ||
@@ -156,3 +156,3 @@ this.ws.once('open', handleOpen); | ||
dispose() { | ||
this.ws.close(3000, 'Disposing console connection.'); | ||
this.ws.close(1000, 'Disposing console connection.'); | ||
this.events.removeAllListeners(); | ||
@@ -159,0 +159,0 @@ } |
@@ -25,6 +25,2 @@ import type { Client } from '../Client'; | ||
/** | ||
* Takes a WebSocket instance and registers event handlers and timers to manage it. | ||
*/ | ||
private registerEventHandlers; | ||
/** | ||
* Sends a ping on the WebSocket. | ||
@@ -31,0 +27,0 @@ */ |
@@ -27,3 +27,2 @@ "use strict"; | ||
this.ws = await this.createWebSocket(this.client.accessToken); | ||
await this.registerEventHandlers(this.ws); | ||
} | ||
@@ -34,13 +33,95 @@ /** | ||
async createWebSocket(accessToken) { | ||
this.logger.debug('Creating new WebSocket.'); | ||
const headers = { | ||
'Content-Type': 'application/json', | ||
'x-api-key': this.client.config.xApiKey, | ||
'User-Agent': this.client.config.clientId, | ||
'Authorization': `Bearer ${accessToken}` | ||
}; | ||
this.logger.debug('Configured WebSocket headers.', JSON.stringify(headers)); | ||
let ws; | ||
try { | ||
ws = new ws_1.WebSocket(this.client.config.webSocketUrl, { headers }); | ||
return await new Promise((resolve, reject) => { | ||
const that = this; | ||
function handleError(error) { | ||
that.logger.error('An error occurred on the WebSocket.', error.message); | ||
/** | ||
* If errors happen before the WebSocket connection is opened, it's likely | ||
* that the WebSocket won't open anymore. In this case we want to reject | ||
* so that we can trigger a recovery. | ||
* | ||
* In other cases, errors will happen during a connection. If that results | ||
* in the WebSocket closing, we can handle this in the onClose handler, | ||
* because supposedly not all errors result in a disconnect. | ||
* | ||
* See: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState | ||
*/ | ||
if (this.readyState !== 1) { | ||
this.removeAllListeners(); | ||
clearInterval(that.pingInterval); | ||
reject(error); | ||
} | ||
} | ||
function handlePing(data) { | ||
that.logger.debug('Received WebSocket ping.', data.toString()); | ||
this.pong(data); | ||
} | ||
function handlePong(data) { | ||
that.logger.debug('Received WebSocket pong.', data.toString()); | ||
} | ||
async function handleClose(code, reason) { | ||
that.logger.debug(`WebSocket is closing with code ${code}: ${reason.toString()}`); | ||
this.removeAllListeners(); | ||
clearInterval(that.pingInterval); | ||
/* Migrations close WebSocket with code 3000 or 3001. */ | ||
if (code !== 3000 && code !== 3001) { | ||
that.logger.error(`WebSocket closed abnormally with code ${code}: ${reason}`); | ||
that.logger.info('Restarting WebSocket and subscriptions.'); | ||
await that.recoverWebSocket(); | ||
} | ||
} | ||
function handleMessage(data, isBinary) { | ||
if (isBinary) { | ||
// This should never happen. There is no Alta documentation about binary data being sent through WebSockets. | ||
that.logger.error('Puking horses! 🐴🐴🤮'); // https://thepetwiki.com/wiki/do_horses_vomit/ | ||
that.logger.debug('Received binary data on WebSocket.', data.toString()); | ||
return; | ||
} | ||
const message = JSON.parse(data.toString()); | ||
/* Handle messages during migration. */ | ||
if (typeof that.resolveHalted !== 'undefined') { | ||
that.events.emit('migrate', message); | ||
return; | ||
} | ||
if (typeof message.content === 'undefined') { | ||
that.logger.error(`Received a message with ID ${message.id} but no content.`, JSON.stringify(message)); | ||
return; | ||
} | ||
that.logger.debug(`Received ${message.event} message with ID ${message.id}.`, JSON.stringify(message)); | ||
const eventName = message.id === 0 ? `${message.event}/${message.key}` : `message-${message.id}`; | ||
that.events.emit(eventName, { | ||
...message, | ||
content: message.content.length > 0 ? JSON.parse(message.content) : message.content | ||
}); | ||
} | ||
function handleOpen() { | ||
that.logger.debug('WebSocket opened.'); | ||
that.logger.debug('Registering WebSocket event handlers.'); | ||
this.on('ping', handlePing); | ||
this.on('pong', handlePong); | ||
this.on('message', handleMessage); | ||
that.logger.debug('Registering WebSocket ping interval.'); | ||
clearInterval(that.pingInterval); | ||
that.pingInterval = setInterval(() => { | ||
that.ping(this); | ||
}, that.client.config.webSocketPingInterval); | ||
resolve(ws); | ||
} | ||
this.logger.debug('Creating new WebSocket.'); | ||
const headers = { | ||
'Content-Type': 'application/json', | ||
'x-api-key': this.client.config.xApiKey, | ||
'User-Agent': this.client.config.clientId, | ||
'Authorization': `Bearer ${accessToken}` | ||
}; | ||
this.logger.debug('Configured WebSocket headers.', JSON.stringify(headers)); | ||
const ws = new ws_1.WebSocket(this.client.config.webSocketUrl, { headers }); | ||
ws.on('error', handleError); | ||
ws.once('close', handleClose); | ||
ws.once('open', handleOpen); | ||
this.logger.debug('Created new WebSocket.'); | ||
clearTimeout(this.migrationDelay); | ||
this.migrationDelay = setTimeout(this.migrate.bind(this), this.client.config.webSocketMigrationInterval); | ||
}); | ||
} | ||
@@ -52,79 +133,4 @@ catch (error) { | ||
} | ||
this.logger.debug('Created new WebSocket.'); | ||
clearTimeout(this.migrationDelay); | ||
this.migrationDelay = setTimeout(this.migrate.bind(this), this.client.config.webSocketMigrationInterval); | ||
return ws; | ||
} | ||
/** | ||
* Takes a WebSocket instance and registers event handlers and timers to manage it. | ||
*/ | ||
registerEventHandlers(ws) { | ||
return new Promise(resolve => { | ||
const that = this; | ||
function handleError(error) { | ||
that.logger.error('An error occurred on the WebSocket.', error.message); | ||
} | ||
function handlePing(data) { | ||
that.logger.debug('Received WebSocket ping.', data.toString()); | ||
this.pong(data); | ||
} | ||
function handlePong(data) { | ||
that.logger.debug('Received WebSocket pong.', data.toString()); | ||
} | ||
async function handleClose(code, reason) { | ||
that.logger.debug(`WebSocket is closing with code ${code}: ${reason.toString()}`); | ||
this.off('error', handleError); | ||
this.off('ping', handlePing); | ||
this.off('pong', handlePong); | ||
clearInterval(that.pingInterval); | ||
/* Migrations close WebSocket with code 3000 or 3001. */ | ||
if (code !== 3000 && code !== 3001) { | ||
that.logger.error(`WebSocket closed abnormally with code ${code}: ${reason}`); | ||
that.logger.info('Restarting WebSocket and subscriptions.'); | ||
await that.recoverWebSocket(); | ||
} | ||
} | ||
function handleMessage(data, isBinary) { | ||
if (isBinary) { | ||
// This should never happen. There is no Alta documentation about binary data being sent through WebSockets. | ||
that.logger.error('Puking horses! 🐴🐴🤮'); // https://thepetwiki.com/wiki/do_horses_vomit/ | ||
that.logger.debug('Received binary data on WebSocket.', data.toString()); | ||
return; | ||
} | ||
const message = JSON.parse(data.toString()); | ||
/* Handle messages during migration. */ | ||
if (typeof that.resolveHalted !== 'undefined') { | ||
that.events.emit('migrate', message); | ||
return; | ||
} | ||
if (typeof message.content === 'undefined') { | ||
that.logger.error(`Received a message with ID ${message.id} but no content.`, JSON.stringify(message)); | ||
return; | ||
} | ||
that.logger.debug(`Received ${message.event} message with ID ${message.id}.`, JSON.stringify(message)); | ||
const eventName = message.id === 0 ? `${message.event}/${message.key}` : `message-${message.id}`; | ||
that.events.emit(eventName, { | ||
...message, | ||
content: message.content.length > 0 ? JSON.parse(message.content) : message.content | ||
}); | ||
} | ||
function handleOpen() { | ||
that.logger.debug('WebSocket opened.'); | ||
that.logger.debug('Registering WebSocket event handlers.'); | ||
this.on('error', handleError); | ||
this.on('ping', handlePing); | ||
this.on('pong', handlePong); | ||
this.on('message', handleMessage); | ||
that.logger.debug('Registering WebSocket ping interval.'); | ||
clearInterval(that.pingInterval); | ||
that.pingInterval = setInterval(() => { | ||
that.ping(this); | ||
}, that.client.config.webSocketPingInterval); | ||
resolve(); | ||
} | ||
ws.once('close', handleClose); | ||
ws.once('open', handleOpen); | ||
}); | ||
} | ||
/** | ||
* Sends a ping on the WebSocket. | ||
@@ -177,3 +183,2 @@ */ | ||
this.ws = await this.createWebSocket(this.client.accessToken); | ||
await this.registerEventHandlers(this.ws); | ||
/* Send the migration token over new WebSocket. */ | ||
@@ -204,3 +209,2 @@ try { | ||
oldWs.close(3000, 'Migration completed.'); | ||
oldWs.removeAllListeners(); | ||
this.logger.info(`Closed old WebSocket.`); | ||
@@ -272,3 +276,2 @@ } | ||
this.ws = await this.createWebSocket(this.client.accessToken); | ||
await this.registerEventHandlers(this.ws); | ||
/* Unblock all backed-up messages. */ | ||
@@ -275,0 +278,0 @@ this.clearHalted(); |
{ | ||
"name": "att-client", | ||
"version": "0.2.23", | ||
"version": "0.2.24", | ||
"description": "Node bot library for A Township Tale, a VR game by Alta Reality Pty Ltd.", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
102751
2382