att-client
Advanced tools
Comparing version 0.5.1 to 0.5.2-beta.1
@@ -97,2 +97,3 @@ "use strict"; | ||
logVerbosity: configuredLogVerbosity, | ||
maxMissedServerHeartbeats: config.maxMissedServerHeartbeats ?? constants_js_1.DEFAULTS.maxMissedServerHeartbeats, | ||
maxSubscriptionsPerWebSocket: config.maxSubscriptionsPerWebSocket ?? constants_js_1.DEFAULTS.maxSubscriptionsPerWebSocket, | ||
@@ -102,3 +103,3 @@ maxWorkerConcurrency: config.maxWorkerConcurrency ?? constants_js_1.DEFAULTS.maxWorkerConcurrency, | ||
serverConnectionRecoveryDelay: config.serverConnectionRecoveryDelay ?? constants_js_1.DEFAULTS.serverConnectionRecoveryDelay, | ||
serverHeartbeatTimeout: config.serverHeartbeatTimeout ?? constants_js_1.DEFAULTS.serverHeartbeatTimeout, | ||
serverHeartbeatInterval: config.serverHeartbeatInterval ?? constants_js_1.DEFAULTS.serverHeartbeatInterval, | ||
supportedServerFleets: config.supportedServerFleets ?? constants_js_1.DEFAULTS.supportedServerFleets, | ||
@@ -105,0 +106,0 @@ tokenUrl: config.tokenUrl ?? constants_js_1.DEFAULTS.tokenUrl, |
@@ -7,12 +7,13 @@ "use strict"; | ||
name: 'att-client', | ||
version: '0.5.1' | ||
version: '0.5.2-beta.1' | ||
}; | ||
const SECOND = 1000; | ||
const MINUTE = 60 * SECOND; | ||
const MAX_MISSED_SERVER_HEARTBEATS = 3; | ||
const MAX_SUBSCRIPTIONS_PER_WEBSOCKET = 500; | ||
const MAX_WORKER_CONCURRENCY = 5; | ||
const MAX_SUBSCRIPTIONS_PER_WEBSOCKET = 500; | ||
exports.MAX_WORKER_CONCURRENCY_WARNING = 10; | ||
const REST_BASE_URL = 'https://webapi.townshiptale.com/api'; | ||
const SERVER_CONNECTION_RECOVERY_DELAY = 10 * SECOND; | ||
const SERVER_HEARTBEAT_TIMEOUT = 10 * MINUTE; | ||
const SERVER_HEARTBEAT_INTERVAL = 20 * SECOND; | ||
const SUPPORTED_SERVER_FLEETS = ['att-release', 'att-quest']; | ||
@@ -37,6 +38,7 @@ const TOKEN_URL = 'https://accounts.townshiptale.com/connect/token'; | ||
restBaseUrl: REST_BASE_URL, | ||
maxMissedServerHeartbeats: MAX_MISSED_SERVER_HEARTBEATS, | ||
maxSubscriptionsPerWebSocket: MAX_SUBSCRIPTIONS_PER_WEBSOCKET, | ||
maxWorkerConcurrency: MAX_WORKER_CONCURRENCY, | ||
maxSubscriptionsPerWebSocket: MAX_SUBSCRIPTIONS_PER_WEBSOCKET, | ||
serverConnectionRecoveryDelay: SERVER_CONNECTION_RECOVERY_DELAY, | ||
serverHeartbeatTimeout: SERVER_HEARTBEAT_TIMEOUT, | ||
serverHeartbeatInterval: SERVER_HEARTBEAT_INTERVAL, | ||
supportedServerFleets: SUPPORTED_SERVER_FLEETS, | ||
@@ -43,0 +45,0 @@ tokenUrl: TOKEN_URL, |
@@ -14,2 +14,4 @@ "use strict"; | ||
servers; | ||
keepAlive; | ||
missedHeartbeats; | ||
userId; | ||
@@ -21,2 +23,4 @@ constructor(client, group, member) { | ||
this.id = group.id; | ||
this.keepAlive = undefined; | ||
this.missedHeartbeats = 0; | ||
this.name = group.name ?? ''; | ||
@@ -75,2 +79,12 @@ this.permissions = this.getPermissions(group, member); | ||
}), | ||
this.client.subscriptions.subscribe('group-server-heartbeat', this.id.toString(), async (message) => { | ||
try { | ||
const status = message.content; | ||
this.client.logger.debug(`Heartbeat for server ${status.id} (${status.name}).`, JSON.stringify(status)); | ||
this.handleHeartbeat(status); | ||
} | ||
catch (error) { | ||
this.client.logger.error(`Error while handling server heartbeat: ${error.message}`); | ||
} | ||
}), | ||
this.client.subscriptions.subscribe('group-server-create', this.id.toString(), _unstableMessage => { | ||
@@ -137,2 +151,22 @@ try { | ||
} | ||
async handleHeartbeat(status) { | ||
if (status.is_online) { | ||
this.missedHeartbeats = 0; | ||
clearTimeout(this.keepAlive); | ||
this.keepAlive = setInterval(() => { | ||
const serverId = status.id; | ||
const server = this.servers[serverId]; | ||
if (typeof server === 'undefined') { | ||
this.client.logger.error(`Server ${serverId} not found in group ${this.id} (${this.name}).`); | ||
return; | ||
} | ||
this.client.logger.info(`No heartbeat received for server ${serverId} (${server.name}) in the last ${this.client.config.serverHeartbeatInterval * ++this.missedHeartbeats} ms.`); | ||
if (this.missedHeartbeats >= this.client.config.maxMissedServerHeartbeats) { | ||
this.client.logger.info(`Maximum missed heartbeats reached for server ${serverId} (${server.name}). Closing connection.`); | ||
server.disconnect(); | ||
} | ||
}, this.client.config.serverHeartbeatInterval); | ||
} | ||
this.manageServerConnection(status); | ||
} | ||
async manageServerConnection(status) { | ||
@@ -148,5 +182,3 @@ const serverId = status.id; | ||
const mayConnect = hasConsolePermission && isSupportedServerFleet; | ||
const lastHeartbeatAt = +new Date(status.online_ping ?? '2022-06-01T00:00:00.000Z'); | ||
const timeSinceLastHeartbeat = Date.now() - lastHeartbeatAt; | ||
const isServerOnline = timeSinceLastHeartbeat < this.client.config.serverHeartbeatTimeout; | ||
const isServerOnline = status.is_online; | ||
const hasOnlinePlayers = status.online_players.length > 0; | ||
@@ -153,0 +185,0 @@ if (server.status === 'disconnected' && mayConnect && isServerOnline && hasOnlinePlayers) { |
@@ -85,6 +85,6 @@ "use strict"; | ||
if (typeof message.content === 'undefined') { | ||
that.client.logger.error(`Received a message with ID ${message.id} but no content.`, JSON.stringify(message)); | ||
that.client.logger.error(`Received a message with ID ${that.instanceId}-${message.id} but no content.`, JSON.stringify(message)); | ||
return; | ||
} | ||
that.client.logger.debug(`Received ${message.event} message with ID ${message.id}.`, JSON.stringify(message)); | ||
that.client.logger.debug(`Received ${message.event} message with ID ${that.instanceId}-${message.id}.`, JSON.stringify(message)); | ||
const eventName = message.id === 0 ? `${message.event}/${message.key}` : `message-${message.id}`; | ||
@@ -91,0 +91,0 @@ that.events.emit(eventName, { |
@@ -91,2 +91,3 @@ import { createHash } from 'node:crypto'; | ||
logVerbosity: configuredLogVerbosity, | ||
maxMissedServerHeartbeats: config.maxMissedServerHeartbeats ?? DEFAULTS.maxMissedServerHeartbeats, | ||
maxSubscriptionsPerWebSocket: config.maxSubscriptionsPerWebSocket ?? DEFAULTS.maxSubscriptionsPerWebSocket, | ||
@@ -96,3 +97,3 @@ maxWorkerConcurrency: config.maxWorkerConcurrency ?? DEFAULTS.maxWorkerConcurrency, | ||
serverConnectionRecoveryDelay: config.serverConnectionRecoveryDelay ?? DEFAULTS.serverConnectionRecoveryDelay, | ||
serverHeartbeatTimeout: config.serverHeartbeatTimeout ?? DEFAULTS.serverHeartbeatTimeout, | ||
serverHeartbeatInterval: config.serverHeartbeatInterval ?? DEFAULTS.serverHeartbeatInterval, | ||
supportedServerFleets: config.supportedServerFleets ?? DEFAULTS.supportedServerFleets, | ||
@@ -99,0 +100,0 @@ tokenUrl: config.tokenUrl ?? DEFAULTS.tokenUrl, |
import './Logger/index.js'; | ||
export const AGENT = { | ||
name: 'att-client', | ||
version: '0.5.1' | ||
version: '0.5.2-beta.1' | ||
}; | ||
const SECOND = 1000; | ||
const MINUTE = 60 * SECOND; | ||
const MAX_MISSED_SERVER_HEARTBEATS = 3; | ||
const MAX_SUBSCRIPTIONS_PER_WEBSOCKET = 500; | ||
const MAX_WORKER_CONCURRENCY = 5; | ||
const MAX_SUBSCRIPTIONS_PER_WEBSOCKET = 500; | ||
export const MAX_WORKER_CONCURRENCY_WARNING = 10; | ||
const REST_BASE_URL = 'https://webapi.townshiptale.com/api'; | ||
const SERVER_CONNECTION_RECOVERY_DELAY = 10 * SECOND; | ||
const SERVER_HEARTBEAT_TIMEOUT = 10 * MINUTE; | ||
const SERVER_HEARTBEAT_INTERVAL = 20 * SECOND; | ||
const SUPPORTED_SERVER_FLEETS = ['att-release', 'att-quest']; | ||
@@ -33,6 +34,7 @@ const TOKEN_URL = 'https://accounts.townshiptale.com/connect/token'; | ||
restBaseUrl: REST_BASE_URL, | ||
maxMissedServerHeartbeats: MAX_MISSED_SERVER_HEARTBEATS, | ||
maxSubscriptionsPerWebSocket: MAX_SUBSCRIPTIONS_PER_WEBSOCKET, | ||
maxWorkerConcurrency: MAX_WORKER_CONCURRENCY, | ||
maxSubscriptionsPerWebSocket: MAX_SUBSCRIPTIONS_PER_WEBSOCKET, | ||
serverConnectionRecoveryDelay: SERVER_CONNECTION_RECOVERY_DELAY, | ||
serverHeartbeatTimeout: SERVER_HEARTBEAT_TIMEOUT, | ||
serverHeartbeatInterval: SERVER_HEARTBEAT_INTERVAL, | ||
supportedServerFleets: SUPPORTED_SERVER_FLEETS, | ||
@@ -39,0 +41,0 @@ tokenUrl: TOKEN_URL, |
@@ -11,2 +11,4 @@ import { TypedEmitter } from '@mdingena/tiny-typed-emitter'; | ||
servers; | ||
keepAlive; | ||
missedHeartbeats; | ||
userId; | ||
@@ -18,2 +20,4 @@ constructor(client, group, member) { | ||
this.id = group.id; | ||
this.keepAlive = undefined; | ||
this.missedHeartbeats = 0; | ||
this.name = group.name ?? ''; | ||
@@ -72,2 +76,12 @@ this.permissions = this.getPermissions(group, member); | ||
}), | ||
this.client.subscriptions.subscribe('group-server-heartbeat', this.id.toString(), async (message) => { | ||
try { | ||
const status = message.content; | ||
this.client.logger.debug(`Heartbeat for server ${status.id} (${status.name}).`, JSON.stringify(status)); | ||
this.handleHeartbeat(status); | ||
} | ||
catch (error) { | ||
this.client.logger.error(`Error while handling server heartbeat: ${error.message}`); | ||
} | ||
}), | ||
this.client.subscriptions.subscribe('group-server-create', this.id.toString(), _unstableMessage => { | ||
@@ -134,2 +148,22 @@ try { | ||
} | ||
async handleHeartbeat(status) { | ||
if (status.is_online) { | ||
this.missedHeartbeats = 0; | ||
clearTimeout(this.keepAlive); | ||
this.keepAlive = setInterval(() => { | ||
const serverId = status.id; | ||
const server = this.servers[serverId]; | ||
if (typeof server === 'undefined') { | ||
this.client.logger.error(`Server ${serverId} not found in group ${this.id} (${this.name}).`); | ||
return; | ||
} | ||
this.client.logger.info(`No heartbeat received for server ${serverId} (${server.name}) in the last ${this.client.config.serverHeartbeatInterval * ++this.missedHeartbeats} ms.`); | ||
if (this.missedHeartbeats >= this.client.config.maxMissedServerHeartbeats) { | ||
this.client.logger.info(`Maximum missed heartbeats reached for server ${serverId} (${server.name}). Closing connection.`); | ||
server.disconnect(); | ||
} | ||
}, this.client.config.serverHeartbeatInterval); | ||
} | ||
this.manageServerConnection(status); | ||
} | ||
async manageServerConnection(status) { | ||
@@ -145,5 +179,3 @@ const serverId = status.id; | ||
const mayConnect = hasConsolePermission && isSupportedServerFleet; | ||
const lastHeartbeatAt = +new Date(status.online_ping ?? '2022-06-01T00:00:00.000Z'); | ||
const timeSinceLastHeartbeat = Date.now() - lastHeartbeatAt; | ||
const isServerOnline = timeSinceLastHeartbeat < this.client.config.serverHeartbeatTimeout; | ||
const isServerOnline = status.is_online; | ||
const hasOnlinePlayers = status.online_players.length > 0; | ||
@@ -150,0 +182,0 @@ if (server.status === 'disconnected' && mayConnect && isServerOnline && hasOnlinePlayers) { |
@@ -82,6 +82,6 @@ import { EventEmitter } from 'node:events'; | ||
if (typeof message.content === 'undefined') { | ||
that.client.logger.error(`Received a message with ID ${message.id} but no content.`, JSON.stringify(message)); | ||
that.client.logger.error(`Received a message with ID ${that.instanceId}-${message.id} but no content.`, JSON.stringify(message)); | ||
return; | ||
} | ||
that.client.logger.debug(`Received ${message.event} message with ID ${message.id}.`, JSON.stringify(message)); | ||
that.client.logger.debug(`Received ${message.event} message with ID ${that.instanceId}-${message.id}.`, JSON.stringify(message)); | ||
const eventName = message.id === 0 ? `${message.event}/${message.key}` : `message-${message.id}`; | ||
@@ -88,0 +88,0 @@ that.events.emit(eventName, { |
@@ -6,2 +6,3 @@ type ServerOnlinePlayers = { | ||
export type ServerFleet = 'att-release' | 'att-quest'; | ||
type JoinType = 'PrivateGroup' | 'Public' | 'OpenGroup' | 'SupporterOnly' | 'WorldInstance' | 'PublicGroup'; | ||
export type ServerInfo = { | ||
@@ -27,3 +28,9 @@ id: number; | ||
up_time: string; | ||
join_type: JoinType; | ||
player_count: number; | ||
player_limit: number; | ||
created_at: string; | ||
is_online: boolean; | ||
transport_system: number; | ||
}; | ||
export {}; |
@@ -10,2 +10,3 @@ import type { ServerFleet } from '../Api/index.js'; | ||
logVerbosity?: Verbosity; | ||
maxMissedServerHeartbeats?: number; | ||
maxSubscriptionsPerWebSocket?: number; | ||
@@ -15,3 +16,3 @@ maxWorkerConcurrency?: number; | ||
serverConnectionRecoveryDelay?: number; | ||
serverHeartbeatTimeout?: number; | ||
serverHeartbeatInterval?: number; | ||
supportedServerFleets?: ServerFleet[]; | ||
@@ -18,0 +19,0 @@ tokenUrl?: string; |
@@ -23,2 +23,4 @@ import type { GroupInfo, GroupMemberInfo } from '../Api/schemas/index.js'; | ||
servers: Servers; | ||
private keepAlive; | ||
private missedHeartbeats; | ||
private userId; | ||
@@ -32,2 +34,3 @@ constructor(client: Client, group: GroupInfo, member: GroupMemberInfo); | ||
private getPermissions; | ||
private handleHeartbeat; | ||
private manageServerConnection; | ||
@@ -34,0 +37,0 @@ private addServers; |
@@ -1,1 +0,1 @@ | ||
export type ClientEvent = 'group-member-update' | 'group-server-create' | 'group-server-delete' | 'group-server-status' | 'group-server-update' | 'group-update' | 'me-group-create' | 'me-group-delete' | 'me-group-invite-create' | 'me-group-invite-delete'; | ||
export type ClientEvent = 'group-member-update' | 'group-server-create' | 'group-server-delete' | 'group-server-heartbeat' | 'group-server-status' | 'group-server-update' | 'group-update' | 'me-group-create' | 'me-group-delete' | 'me-group-invite-create' | 'me-group-invite-delete'; |
@@ -12,2 +12,5 @@ import type { GroupInfo, GroupMemberInfo, ServerInfo } from '../Api/schemas/index.js'; | ||
}; | ||
type GroupServerHeartbeatMessage = EventMessage<'group-server-heartbeat'> & { | ||
content: ServerInfo; | ||
}; | ||
type GroupServerStatusMessage = EventMessage<'group-server-status'> & { | ||
@@ -37,3 +40,3 @@ content: ServerInfo; | ||
}; | ||
type ClientEventMessageUnion = GroupMemberUpdateMessage | GroupServerStatusMessage | GroupUpdateMessage | MeGroupCreateMessage | MeGroupDeleteMessage | MeGroupInviteCreateMessage | MeGroupInviteDeleteMessage; | ||
type ClientEventMessageUnion = GroupMemberUpdateMessage | GroupServerHeartbeatMessage | GroupServerStatusMessage | GroupUpdateMessage | MeGroupCreateMessage | MeGroupDeleteMessage | MeGroupInviteCreateMessage | MeGroupInviteDeleteMessage; | ||
export type ClientEventMessage<T> = Extract<ClientEventMessageUnion, { | ||
@@ -40,0 +43,0 @@ event: T; |
@@ -33,3 +33,3 @@ import type { Client } from '../Client/index.js'; | ||
} & { | ||
key: "POST /ws/subscription/group-member-update" | "POST /ws/subscription/group-server-status" | "POST /ws/subscription/group-update" | "POST /ws/subscription/me-group-create" | "POST /ws/subscription/me-group-delete" | "POST /ws/subscription/me-group-invite-create" | "POST /ws/subscription/me-group-invite-delete" | "POST /ws/subscription/group-server-create" | "POST /ws/subscription/group-server-delete" | "POST /ws/subscription/group-server-update"; | ||
key: "POST /ws/subscription/group-member-update" | "POST /ws/subscription/group-server-heartbeat" | "POST /ws/subscription/group-server-status" | "POST /ws/subscription/group-update" | "POST /ws/subscription/me-group-create" | "POST /ws/subscription/me-group-delete" | "POST /ws/subscription/me-group-invite-create" | "POST /ws/subscription/me-group-invite-delete" | "POST /ws/subscription/group-server-create" | "POST /ws/subscription/group-server-delete" | "POST /ws/subscription/group-server-update"; | ||
content: ""; | ||
@@ -42,3 +42,3 @@ })> | undefined; | ||
} & { | ||
key: "DELETE /ws/subscription/group-member-update" | "DELETE /ws/subscription/group-server-status" | "DELETE /ws/subscription/group-update" | "DELETE /ws/subscription/me-group-create" | "DELETE /ws/subscription/me-group-delete" | "DELETE /ws/subscription/me-group-invite-create" | "DELETE /ws/subscription/me-group-invite-delete" | "DELETE /ws/subscription/group-server-create" | "DELETE /ws/subscription/group-server-delete" | "DELETE /ws/subscription/group-server-update"; | ||
key: "DELETE /ws/subscription/group-member-update" | "DELETE /ws/subscription/group-server-heartbeat" | "DELETE /ws/subscription/group-server-status" | "DELETE /ws/subscription/group-update" | "DELETE /ws/subscription/me-group-create" | "DELETE /ws/subscription/me-group-delete" | "DELETE /ws/subscription/me-group-invite-create" | "DELETE /ws/subscription/me-group-invite-delete" | "DELETE /ws/subscription/group-server-create" | "DELETE /ws/subscription/group-server-delete" | "DELETE /ws/subscription/group-server-update"; | ||
content: ""; | ||
@@ -45,0 +45,0 @@ })> | undefined; |
@@ -17,3 +17,3 @@ import type { ClientEvent } from '../Subscriptions/ClientEvent.js'; | ||
} & { | ||
key: "POST /ws/subscription/group-member-update" | "POST /ws/subscription/group-server-status" | "POST /ws/subscription/group-update" | "POST /ws/subscription/me-group-create" | "POST /ws/subscription/me-group-delete" | "POST /ws/subscription/me-group-invite-create" | "POST /ws/subscription/me-group-invite-delete" | "POST /ws/subscription/group-server-create" | "POST /ws/subscription/group-server-delete" | "POST /ws/subscription/group-server-update"; | ||
key: "POST /ws/subscription/group-member-update" | "POST /ws/subscription/group-server-heartbeat" | "POST /ws/subscription/group-server-status" | "POST /ws/subscription/group-update" | "POST /ws/subscription/me-group-create" | "POST /ws/subscription/me-group-delete" | "POST /ws/subscription/me-group-invite-create" | "POST /ws/subscription/me-group-invite-delete" | "POST /ws/subscription/group-server-create" | "POST /ws/subscription/group-server-delete" | "POST /ws/subscription/group-server-update"; | ||
content: ""; | ||
@@ -26,3 +26,3 @@ })>; | ||
} & { | ||
key: "DELETE /ws/subscription/group-member-update" | "DELETE /ws/subscription/group-server-status" | "DELETE /ws/subscription/group-update" | "DELETE /ws/subscription/me-group-create" | "DELETE /ws/subscription/me-group-delete" | "DELETE /ws/subscription/me-group-invite-create" | "DELETE /ws/subscription/me-group-invite-delete" | "DELETE /ws/subscription/group-server-create" | "DELETE /ws/subscription/group-server-delete" | "DELETE /ws/subscription/group-server-update"; | ||
key: "DELETE /ws/subscription/group-member-update" | "DELETE /ws/subscription/group-server-heartbeat" | "DELETE /ws/subscription/group-server-status" | "DELETE /ws/subscription/group-update" | "DELETE /ws/subscription/me-group-create" | "DELETE /ws/subscription/me-group-delete" | "DELETE /ws/subscription/me-group-invite-create" | "DELETE /ws/subscription/me-group-invite-delete" | "DELETE /ws/subscription/group-server-create" | "DELETE /ws/subscription/group-server-delete" | "DELETE /ws/subscription/group-server-update"; | ||
content: ""; | ||
@@ -29,0 +29,0 @@ })>; |
{ | ||
"name": "att-client", | ||
"version": "0.5.1", | ||
"version": "0.5.2-beta.1", | ||
"description": "Node bot library for A Township Tale, a VR game by Alta", | ||
@@ -20,3 +20,2 @@ "homepage": "https://github.com/mdingena/att-client#readme", | ||
"scripts": { | ||
"test": "node --loader ts-node/esm -r dotenv/config test.ts", | ||
"lint": "eslint \"src/**/*\"", | ||
@@ -23,0 +22,0 @@ "compile": "tsc --noEmit", |
179182
3944