diff --git a/README.md b/README.md index b0696be..a874c36 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,9 @@ NAME=webmud3 # Optional | defaults to 'webmud3' TELNET_HOST=127.0.0.1 # Required | the IP of your MUD TELNET_PORT=23 # Required | the PORT of your MUD TELNET_TLS=false # Optional | defaults to 'false' | set this to true if you want a secure connection +TELNET_KEEPALIVE_DELAY=30000 # Optional | defaults to 30000 (30s) | TCP keepalive interval for telnet connections +SOCKET_PING_INTERVAL=25000 # Optional | defaults to 25000 (25s) | Socket.IO ping interval +SOCKET_PING_TIMEOUT=20000 # Optional | defaults to 20000 (20s) | Socket.IO ping timeout SOCKET_TIMEOUT=900000 # Optional | defaults to 900000 (15 min) | timeout for any lost frontend <-> backend connection SOCKET_ROOT=/socket.io # Required | URL for the socket connection. e.g. 'https://mud.example.com/socket.io' ENVIRONMENT='development' # Optional | accepts values 'development' or 'production' | defaults to 'production' | Enables Debug REST Endpoint /api/info and allows for permissive CORS if set to 'development' diff --git a/backend/src/core/environment/environment.ts b/backend/src/core/environment/environment.ts index 585c725..995be65 100644 --- a/backend/src/core/environment/environment.ts +++ b/backend/src/core/environment/environment.ts @@ -19,7 +19,10 @@ export class Environment implements IEnvironment { public readonly telnetTLS: boolean; public readonly projectRoot: string; public readonly socketRoot: string; + public readonly socketPingInterval: number; + public readonly socketPingTimeout: number; public readonly socketTimeout: number; + public readonly telnetKeepAliveDelay: number; public readonly environment: 'production' | 'development'; public readonly name: string; public readonly corsAllowList: string[]; @@ -49,10 +52,22 @@ export class Environment implements IEnvironment { this.socketRoot = String(getEnvironmentVariable('SOCKET_ROOT')); + this.socketPingInterval = Number( + getEnvironmentVariable('SOCKET_PING_INTERVAL', false, '25000'), + ); + + this.socketPingTimeout = Number( + getEnvironmentVariable('SOCKET_PING_TIMEOUT', false, '20000'), + ); + this.socketTimeout = Number( getEnvironmentVariable('SOCKET_TIMEOUT', false, '900000'), ); + this.telnetKeepAliveDelay = Number( + getEnvironmentVariable('TELNET_KEEPALIVE_DELAY', false, '30000'), + ); + const environment = String( getEnvironmentVariable('ENVIRONMENT', false, 'production'), ).toLocaleLowerCase(); diff --git a/backend/src/core/environment/types/environment-keys.ts b/backend/src/core/environment/types/environment-keys.ts index 4c181d9..368647e 100644 --- a/backend/src/core/environment/types/environment-keys.ts +++ b/backend/src/core/environment/types/environment-keys.ts @@ -5,6 +5,9 @@ export type EnvironmentKeys = | 'TELNET_HOST' // Required | the IP of your MUD | 'TELNET_PORT' // Required | the PORT of your MUD | 'TELNET_TLS' // Optional | defaults to 'false' | set this to true if you want a secure connection + | 'TELNET_KEEPALIVE_DELAY' // in milliseconds | default: 30000 (30s) | TCP keepalive interval for telnet connections + | 'SOCKET_PING_INTERVAL' // in milliseconds | default: 25000 (25s) | Socket.IO ping interval + | 'SOCKET_PING_TIMEOUT' // in milliseconds | default: 20000 (20s) | Socket.IO ping timeout | 'SOCKET_TIMEOUT' // in milliseconds | default: 900000 (15 min) | determines how long messages are buffed for the disconnected frontend and when the telnet connection is closed | 'SOCKET_ROOT' // Required | URL for the socket connection. e.g. 'https://mud.example.com/socket.io' | 'ENVIRONMENT' // Optional | accepts values 'development' or 'production' | defaults to 'production' | Enables Debug REST Endpoint /api/info and allows for permissive CORS if set to 'development' diff --git a/backend/src/core/environment/types/environment.ts b/backend/src/core/environment/types/environment.ts index 49e7b50..4188798 100644 --- a/backend/src/core/environment/types/environment.ts +++ b/backend/src/core/environment/types/environment.ts @@ -5,7 +5,10 @@ export interface IEnvironment { readonly name: string; readonly projectRoot: string; readonly socketRoot: string; + readonly socketPingInterval: number; + readonly socketPingTimeout: number; readonly socketTimeout: number; + readonly telnetKeepAliveDelay: number; readonly environment: 'production' | 'development'; readonly corsAllowList: string[]; } diff --git a/backend/src/core/sockets/socket-manager.ts b/backend/src/core/sockets/socket-manager.ts index 81f1fb1..f81be46 100644 --- a/backend/src/core/sockets/socket-manager.ts +++ b/backend/src/core/sockets/socket-manager.ts @@ -34,11 +34,15 @@ export class SocketManager extends Server< clientName: string; }, ) { + const environment = Environment.getInstance(); + super(server, { path: managerOptions.socketRoot, - transports: ['websocket'], + transports: ['websocket', 'polling'], + pingInterval: environment.socketPingInterval, + pingTimeout: environment.socketPingTimeout, connectionStateRecovery: { - maxDisconnectionDuration: Environment.getInstance().socketTimeout, + maxDisconnectionDuration: environment.socketTimeout, }, }); @@ -286,6 +290,7 @@ export class SocketManager extends Server< this.managerOptions.clientName, { initialViewPort, + keepAliveDelayMs: Environment.getInstance().telnetKeepAliveDelay, }, ); diff --git a/backend/src/features/telnet/telnet-client.ts b/backend/src/features/telnet/telnet-client.ts index c4bf227..e840b0d 100644 --- a/backend/src/features/telnet/telnet-client.ts +++ b/backend/src/features/telnet/telnet-client.ts @@ -86,7 +86,10 @@ export class TelnetClient extends EventEmitter { telnetPort: number, useTls: boolean, clientName: string, - extraOptions?: { initialViewPort: { columns: number; rows: number } }, + extraOptions?: { + initialViewPort: { columns: number; rows: number }; + keepAliveDelayMs?: number; + }, ) { super(); @@ -96,6 +99,20 @@ export class TelnetClient extends EventEmitter { telnetPort, ); + if (extraOptions?.keepAliveDelayMs !== undefined) { + // Enable TCP keepalive to reduce idle disconnects on intermediaries. + telnetConnection.setKeepAlive(true, extraOptions.keepAliveDelayMs); + } + + telnetConnection.on('error', (error) => { + logger.error( + `[${this.socketId}] [Telnet-Client] Telnet socket error`, + { + error, + }, + ); + }); + if (useTls) { logger.info( `[${this.socketId}] [Telnet-Client] created https connection for telnet`, diff --git a/frontend/src/app/features/sockets/sockets.service.ts b/frontend/src/app/features/sockets/sockets.service.ts index ac5279f..ebc567e 100644 --- a/frontend/src/app/features/sockets/sockets.service.ts +++ b/frontend/src/app/features/sockets/sockets.service.ts @@ -29,6 +29,8 @@ export class SocketsService { private readonly inputQueue: string[] = []; private isReconnecting = false; private sessionToken: string; + // Forces a fresh session token after reconnect failed to avoid reusing a dead backend session. + private forceNewSession = false; public onMudConnect = new EventEmitter(); // Emits isNewConnection public onMudDisconnect = new EventEmitter(); @@ -54,7 +56,7 @@ export class SocketsService { this.manager = new Manager(socketUrl, { path: socketNamespace, - transports: ['websocket'], + transports: ['websocket', 'polling'], reconnectionAttempts: Infinity, reconnection: true, }); @@ -146,6 +148,11 @@ export class SocketsService { columns: number; rows: number; }): void { + if (this.forceNewSession) { + this.resetSessionToken(); + this.forceNewSession = false; + } + console.log( `[Sockets] Sockets-Service: 'connectToMud' with sessionToken: ${this.sessionToken}`, ); @@ -291,6 +298,7 @@ export class SocketsService { private handleReconnectFailed = () => { this.connectedToServer.next(false); + this.forceNewSession = true; console.error('[Sockets] Sockets-Service: Reconnect Failed'); }; @@ -392,4 +400,15 @@ export class SocketsService { }, ); } + + /** + * Resets the session token to force a clean backend session on next connect. + * This is used after a reconnect failure to avoid reusing a potentially dead backend session. + */ + private resetSessionToken(): void { + const newToken = this.generateUUID(); + this.sessionToken = newToken; + this.saveSessionToken(newToken); + this.socket.auth = { sessionToken: newToken }; + } } diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index fb92c79..612cc3a 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -7,7 +7,7 @@ import { Environment } from './environment.interface'; export const environment: Environment = { production: false, // Change this to your local IP if you want to test on a mobile device in the same network - backendUrl: () => "http://localhost:5000", + backendUrl: () => 'http://localhost:5000', }; /*