Try to keep connections alive for longer
This commit is contained in:
@@ -13,16 +13,26 @@ enum TelnetCommand {
|
||||
GMCP = 201, // Generic MUD Communication Protocol
|
||||
}
|
||||
|
||||
interface MudConnectionOptions {
|
||||
export interface MudConnectionOptions {
|
||||
id: string;
|
||||
host: string;
|
||||
port: number;
|
||||
useSSL?: boolean;
|
||||
id: string;
|
||||
}
|
||||
|
||||
// Connection persistence state
|
||||
interface ConnectionPersistence {
|
||||
sessionId?: string;
|
||||
reconnectAttempts: number;
|
||||
maxReconnectAttempts: number;
|
||||
reconnectDelay: number;
|
||||
lastDisconnectTime?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* MudConnection - Handles a single connection to a MUD server
|
||||
* Each instance has its own GMCP handler and maintains its own state
|
||||
* Now supports connection persistence and automatic reconnection
|
||||
*/
|
||||
export class MudConnection extends EventEmitter {
|
||||
private host: string;
|
||||
@@ -36,6 +46,15 @@ export class MudConnection extends EventEmitter {
|
||||
private isInIAC: boolean = false;
|
||||
private inSubnegotiation: boolean = false;
|
||||
public readonly id: string;
|
||||
|
||||
// Connection persistence properties
|
||||
private persistence: ConnectionPersistence = {
|
||||
reconnectAttempts: 0,
|
||||
maxReconnectAttempts: 3,
|
||||
reconnectDelay: 5000 // 5 seconds
|
||||
};
|
||||
private reconnectTimeoutId: number | null = null;
|
||||
private explicitDisconnect: boolean = false;
|
||||
|
||||
constructor(options: MudConnectionOptions) {
|
||||
super();
|
||||
@@ -58,25 +77,25 @@ export class MudConnection extends EventEmitter {
|
||||
*/
|
||||
private setupGmcpEvents(): void {
|
||||
// Forward all GMCP events to listeners of this connection
|
||||
this.gmcpHandler.on('gmcp', (module, data) => {
|
||||
this.gmcpHandler.on('gmcp', (module: string, data: any) => {
|
||||
this.emit('gmcp', module, data);
|
||||
});
|
||||
|
||||
// Forward specific module events (like gmcp:Core.Ping)
|
||||
this.gmcpHandler.on('*', (eventName, ...args) => {
|
||||
this.gmcpHandler.on('*', (eventName: string, ...args: any[]) => {
|
||||
if (eventName.startsWith('gmcp:')) {
|
||||
this.emit(eventName, ...args);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle GMCP events that need special processing
|
||||
this.gmcpHandler.on('playSound', (url, volume, loop) => {
|
||||
this.gmcpHandler.on('playSound', (url: string, volume: number, loop: boolean) => {
|
||||
console.log(`MudConnection forwarding playSound event: ${url}`);
|
||||
this.emit('playSound', { url, volume, loop });
|
||||
});
|
||||
|
||||
// Listen for sendGmcp events from the GMCP handler
|
||||
this.gmcpHandler.on('sendGmcp', (module, data) => {
|
||||
this.gmcpHandler.on('sendGmcp', (module: string, data: any) => {
|
||||
this.sendGmcp(module, data);
|
||||
});
|
||||
}
|
||||
@@ -90,6 +109,9 @@ export class MudConnection extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset explicit disconnect flag
|
||||
this.explicitDisconnect = false;
|
||||
|
||||
// Determine the WebSocket URL based on environment
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
let wsUrl;
|
||||
@@ -102,6 +124,12 @@ export class MudConnection extends EventEmitter {
|
||||
wsUrl = `${wsProtocol}://${window.location.host}/mud-ws?host=${encodeURIComponent(this.host)}&port=${this.port}&useSSL=${this.useSSL}`;
|
||||
}
|
||||
|
||||
// Include session ID in URL if we have one (for reconnection)
|
||||
if (this.persistence.sessionId) {
|
||||
wsUrl += `&sessionId=${encodeURIComponent(this.persistence.sessionId)}`;
|
||||
console.log(`Reconnecting with session ID: ${this.persistence.sessionId}`);
|
||||
}
|
||||
|
||||
console.log(`Connecting to WebSocket server: ${wsUrl}`);
|
||||
|
||||
this.webSocket = new WebSocket(wsUrl);
|
||||
@@ -109,6 +137,7 @@ export class MudConnection extends EventEmitter {
|
||||
|
||||
this.webSocket.onopen = () => {
|
||||
this.connected = true;
|
||||
this.persistence.reconnectAttempts = 0; // Reset reconnect attempts on successful connection
|
||||
console.log(`Connected to ${this.host}:${this.port}`);
|
||||
this.emit('connected');
|
||||
|
||||
@@ -121,6 +150,12 @@ export class MudConnection extends EventEmitter {
|
||||
this.connected = false;
|
||||
console.log(`Disconnected from ${this.host}:${this.port}`);
|
||||
this.emit('disconnected');
|
||||
|
||||
// Handle reconnection if not explicitly disconnected
|
||||
if (!this.explicitDisconnect) {
|
||||
this.persistence.lastDisconnectTime = Date.now();
|
||||
this.handleReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
this.webSocket.onerror = (error) => {
|
||||
@@ -133,9 +168,14 @@ export class MudConnection extends EventEmitter {
|
||||
// Binary data
|
||||
this.handleIncomingData(new Uint8Array(event.data));
|
||||
} else if (typeof event.data === 'string') {
|
||||
// Text data - let listeners process it directly
|
||||
// TriggerSystem will handle gagging and replacing in the component
|
||||
this.emit('received', event.data);
|
||||
// Check if this is a system message from the server
|
||||
if (event.data.startsWith('[SYSTEM]')) {
|
||||
this.handleSystemMessage(event.data);
|
||||
} else {
|
||||
// Text data - let listeners process it directly
|
||||
// TriggerSystem will handle gagging and replacing in the component
|
||||
this.emit('received', event.data);
|
||||
}
|
||||
} else if (event.data instanceof Blob) {
|
||||
// Blob data (sometimes WebSockets send this instead of ArrayBuffer)
|
||||
const reader = new FileReader();
|
||||
@@ -176,7 +216,35 @@ export class MudConnection extends EventEmitter {
|
||||
this.emit('sent', text);
|
||||
} catch (error) {
|
||||
console.error('Error sending data:', error);
|
||||
this.emit('error', `Failed to send message: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
this.emit('error', `Failed to send message: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle system messages from the server
|
||||
*/
|
||||
private handleSystemMessage(message: string): void {
|
||||
console.log('Received system message:', message);
|
||||
|
||||
try {
|
||||
// Remove the [SYSTEM] prefix and parse as JSON
|
||||
const jsonStr = message.substring(8); // Remove "[SYSTEM]"
|
||||
const systemData = JSON.parse(jsonStr);
|
||||
|
||||
// Handle session ID updates
|
||||
if (systemData.sessionId) {
|
||||
this.persistence.sessionId = systemData.sessionId;
|
||||
console.log('Updated session ID:', this.persistence.sessionId);
|
||||
}
|
||||
|
||||
// Handle other system messages as needed
|
||||
if (systemData.type === 'session_resumed') {
|
||||
console.log('Session successfully resumed');
|
||||
this.emit('session_resumed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing system message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,10 +252,31 @@ export class MudConnection extends EventEmitter {
|
||||
* Disconnect from the MUD server
|
||||
*/
|
||||
public disconnect(): void {
|
||||
this.explicitDisconnect = true; // Set flag for explicit disconnect
|
||||
|
||||
// Signal to server that this is an explicit disconnect
|
||||
if (this.connected && this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this.webSocket.send('[SYSTEM]{"type":"explicit_disconnect"}');
|
||||
} catch (error) {
|
||||
console.error('Error sending explicit disconnect signal:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.webSocket) {
|
||||
this.webSocket.close();
|
||||
this.webSocket = null;
|
||||
}
|
||||
|
||||
// Clear session ID since we're explicitly disconnecting
|
||||
this.persistence.sessionId = undefined;
|
||||
this.persistence.reconnectAttempts = 0;
|
||||
|
||||
// Clear reconnect timeout if active
|
||||
if (this.reconnectTimeoutId !== null) {
|
||||
clearTimeout(this.reconnectTimeoutId);
|
||||
this.reconnectTimeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -390,4 +479,52 @@ export class MudConnection extends EventEmitter {
|
||||
public isConnected(): boolean {
|
||||
return this.connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle reconnection logic
|
||||
*/
|
||||
private handleReconnect(): void {
|
||||
// If too much time has passed since disconnect, don't attempt to reconnect with session
|
||||
if (this.persistence.lastDisconnectTime &&
|
||||
Date.now() - this.persistence.lastDisconnectTime > 5 * 60 * 1000) { // 5 minutes
|
||||
console.log('Too much time has passed, clearing session for fresh connection');
|
||||
this.persistence.sessionId = undefined;
|
||||
this.persistence.reconnectAttempts = 0;
|
||||
}
|
||||
|
||||
if (this.persistence.reconnectAttempts >= this.persistence.maxReconnectAttempts) {
|
||||
console.log('Max reconnect attempts reached, giving up');
|
||||
this.persistence.sessionId = undefined; // Clear session since we're giving up
|
||||
return;
|
||||
}
|
||||
|
||||
this.persistence.reconnectAttempts++;
|
||||
const delay = this.persistence.reconnectDelay * Math.pow(1.5, this.persistence.reconnectAttempts - 1); // Exponential backoff
|
||||
|
||||
console.log(`Reconnecting in ${delay / 1000} seconds... (Attempt ${this.persistence.reconnectAttempts}/${this.persistence.maxReconnectAttempts})`);
|
||||
|
||||
this.reconnectTimeoutId = window.setTimeout(() => {
|
||||
console.log('Reconnecting...');
|
||||
this.connect();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current session ID
|
||||
*/
|
||||
public getSessionId(): string | undefined {
|
||||
return this.persistence.sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset reconnection state
|
||||
*/
|
||||
public resetReconnectionState(): void {
|
||||
this.persistence.reconnectAttempts = 0;
|
||||
this.persistence.lastDisconnectTime = undefined;
|
||||
if (this.reconnectTimeoutId !== null) {
|
||||
clearTimeout(this.reconnectTimeoutId);
|
||||
this.reconnectTimeoutId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user