2025-04-21 14:12:36 +02:00
import { EventEmitter } from '$lib/utils/EventEmitter' ;
2025-04-22 18:21:10 +02:00
import { GmcpHandler } from '$lib/gmcp/GmcpHandler' ;
2025-07-25 14:18:40 +01:00
import { get } from 'svelte/store' ;
import { connectionSettings } from '$lib/stores/mudStore' ;
2025-04-21 14:12:36 +02:00
// IAC codes for telnet negotiation
enum TelnetCommand {
IAC = 255 , // Interpret As Command
DONT = 254 ,
DO = 253 ,
WONT = 252 ,
WILL = 251 ,
SB = 250 , // Subnegotiation Begin
SE = 240 , // Subnegotiation End
GMCP = 201 , // Generic MUD Communication Protocol
}
2025-07-10 23:09:27 +01:00
export interface MudConnectionOptions {
id : string ;
2025-04-21 14:12:36 +02:00
host : string ;
port : number ;
useSSL? : boolean ;
2025-07-10 23:09:27 +01:00
}
// Connection persistence state
interface ConnectionPersistence {
sessionId? : string ;
reconnectAttempts : number ;
maxReconnectAttempts : number ;
reconnectDelay : number ;
lastDisconnectTime? : number ;
2025-04-21 14:12:36 +02:00
}
2025-07-25 15:11:02 +01:00
// Stored session data in localStorage
interface StoredSessionData {
sessionId : string ;
profileId : string ;
host : string ;
port : number ;
useSSL : boolean ;
lastActivity : number ;
createdAt : number ;
}
2025-04-22 18:21:10 +02:00
/**
* MudConnection - Handles a single connection to a MUD server
* Each instance has its own GMCP handler and maintains its own state
2025-07-10 23:09:27 +01:00
* Now supports connection persistence and automatic reconnection
2025-04-22 18:21:10 +02:00
*/
2025-04-21 14:12:36 +02:00
export class MudConnection extends EventEmitter {
private host : string ;
private port : number ;
private useSSL : boolean ;
private webSocket : WebSocket | null = null ;
2025-04-22 18:21:10 +02:00
private gmcpHandler : GmcpHandler ;
2025-04-21 14:12:36 +02:00
private buffer : number [] = [];
private connected : boolean = false ;
private negotiationBuffer : number [] = [];
private isInIAC : boolean = false ;
private inSubnegotiation : boolean = false ;
2025-04-22 18:21:10 +02:00
public readonly id : string ;
2025-07-10 23:09:27 +01:00
// Connection persistence properties
private persistence : ConnectionPersistence = {
reconnectAttempts : 0 ,
maxReconnectAttempts : 3 ,
reconnectDelay : 5000 // 5 seconds
};
private reconnectTimeoutId : number | null = null ;
private explicitDisconnect : boolean = false ;
2025-04-21 14:12:36 +02:00
constructor ( options : MudConnectionOptions ) {
super ();
this . host = options . host ;
this . port = options . port ;
this . useSSL = options . useSSL || false ;
2025-04-22 18:21:10 +02:00
this . id = options . id ;
// Create GMCP handler
this . gmcpHandler = new GmcpHandler ();
// Set up GMCP event forwarding
this . setupGmcpEvents ();
2025-07-25 15:11:02 +01:00
// Try to restore session from localStorage
this . loadStoredSession ();
2025-04-22 18:21:10 +02:00
console . log ( `MudConnection created for ${ this . host } : ${ this . port } with ID ${ this . id } ` );
}
/**
* Set up event forwarding from GMCP handler
*/
private setupGmcpEvents () : void {
// Forward all GMCP events to listeners of this connection
2025-07-10 23:09:27 +01:00
this . gmcpHandler . on ( 'gmcp' , ( module : string , data : any ) => {
2025-04-22 18:21:10 +02:00
this . emit ( 'gmcp' , module , data );
});
// Forward specific module events (like gmcp:Core.Ping)
2025-07-10 23:09:27 +01:00
this . gmcpHandler . on ( '*' , ( eventName : string , ... args : any []) => {
2025-04-22 18:21:10 +02:00
if ( eventName . startsWith ( 'gmcp:' )) {
this . emit ( eventName , ... args );
}
});
// Handle GMCP events that need special processing
2025-07-10 23:09:27 +01:00
this . gmcpHandler . on ( 'playSound' , ( url : string , volume : number , loop : boolean ) => {
2025-04-22 18:21:10 +02:00
console . log ( `MudConnection forwarding playSound event: ${ url } ` );
this . emit ( 'playSound' , { url , volume , loop });
});
// Listen for sendGmcp events from the GMCP handler
2025-07-10 23:09:27 +01:00
this . gmcpHandler . on ( 'sendGmcp' , ( module : string , data : any ) => {
2025-04-22 18:21:10 +02:00
this . sendGmcp ( module , data );
});
2025-04-21 14:12:36 +02:00
}
/**
* Connect to the MUD server
*/
public connect () : void {
2025-04-22 18:21:10 +02:00
if ( this . connected ) {
console . log ( `Already connected to ${ this . host } : ${ this . port } ` );
2025-04-21 14:12:36 +02:00
return ;
}
2025-07-10 23:09:27 +01:00
// Reset explicit disconnect flag
this . explicitDisconnect = false ;
2025-04-21 23:43:50 +02:00
// Determine the WebSocket URL based on environment
2025-04-21 14:12:36 +02:00
const wsProtocol = window . location . protocol === 'https:' ? 'wss' : 'ws' ;
2025-04-21 23:43:50 +02:00
let wsUrl ;
2025-04-21 14:12:36 +02:00
2025-04-21 23:43:50 +02:00
// In development, use port 3001
if ( window . location . hostname === 'localhost' || window . location . hostname === '127.0.0.1' ) {
wsUrl = ` ${ wsProtocol } :// ${ window . location . hostname } :3001/mud-ws?host= ${ encodeURIComponent ( this . host ) } &port= ${ this . port } &useSSL= ${ this . useSSL } ` ;
} else {
// In production, use the same domain & port as the web app
wsUrl = ` ${ wsProtocol } :// ${ window . location . host } /mud-ws?host= ${ encodeURIComponent ( this . host ) } &port= ${ this . port } &useSSL= ${ this . useSSL } ` ;
}
2025-04-21 14:12:36 +02:00
2025-07-10 23:09:27 +01:00
// 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 } ` );
}
2025-07-25 14:18:40 +01:00
// Include connection settings in URL
const settings = get ( connectionSettings );
wsUrl += `&persistenceTimeout= ${ settings . persistenceTimeoutMinutes } ` ;
wsUrl += `&maxBufferMessages= ${ settings . maxBufferMessages } ` ;
wsUrl += `&maxBufferSizeKB= ${ settings . maxBufferSizeKB } ` ;
2025-04-22 18:21:10 +02:00
console . log ( `Connecting to WebSocket server: ${ wsUrl } ` );
2025-04-21 23:43:50 +02:00
this . webSocket = new WebSocket ( wsUrl );
2025-04-21 14:12:36 +02:00
this . webSocket . binaryType = 'arraybuffer' ;
this . webSocket . onopen = () => {
this . connected = true ;
2025-07-10 23:09:27 +01:00
this . persistence . reconnectAttempts = 0 ; // Reset reconnect attempts on successful connection
2025-04-22 18:21:10 +02:00
console . log ( `Connected to ${ this . host } : ${ this . port } ` );
2025-04-21 14:12:36 +02:00
this . emit ( 'connected' );
2025-07-25 15:11:02 +01:00
// Update stored session activity
this . updateStoredSessionActivity ();
2025-04-21 14:12:36 +02:00
// Send GMCP negotiation upon connection
2025-04-22 18:21:10 +02:00
console . log ( 'Sending GMCP negotiation' );
this . sendIAC ( TelnetCommand . WILL , TelnetCommand . GMCP );
2025-04-21 14:12:36 +02:00
};
this . webSocket . onclose = () => {
this . connected = false ;
2025-04-22 18:21:10 +02:00
console . log ( `Disconnected from ${ this . host } : ${ this . port } ` );
2025-04-21 14:12:36 +02:00
this . emit ( 'disconnected' );
2025-07-10 23:09:27 +01:00
// Handle reconnection if not explicitly disconnected
if ( ! this . explicitDisconnect ) {
this . persistence . lastDisconnectTime = Date . now ();
this . handleReconnect ();
}
2025-04-21 14:12:36 +02:00
};
this . webSocket . onerror = ( error ) => {
console . error ( 'WebSocket error:' , error );
this . emit ( 'error' , `WebSocket error: Connection to ${ this . host } : ${ this . port } failed. Please check your settings and ensure the MUD server is running.` );
};
this . webSocket . onmessage = ( event ) => {
if ( event . data instanceof ArrayBuffer ) {
// Binary data
this . handleIncomingData ( new Uint8Array ( event . data ));
} else if ( typeof event . data === 'string' ) {
2025-07-10 23:09:27 +01:00
// 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
2025-07-25 15:11:02 +01:00
this . updateStoredSessionActivity ();
2025-07-10 23:09:27 +01:00
this . emit ( 'received' , event . data );
}
2025-04-21 14:12:36 +02:00
} else if ( event . data instanceof Blob ) {
// Blob data (sometimes WebSockets send this instead of ArrayBuffer)
const reader = new FileReader ();
reader . onload = () => {
if ( reader . result instanceof ArrayBuffer ) {
this . handleIncomingData ( new Uint8Array ( reader . result ));
}
};
reader . readAsArrayBuffer ( event . data );
}
};
}
/**
* Send text to the MUD server
*/
public send ( text : string ) : void {
if ( ! this . connected ) {
throw new Error ( 'Not connected to MUD server' );
}
if ( ! this . webSocket ) {
throw new Error ( 'WebSocket not initialized' );
}
// Check if the WebSocket is in a valid state for sending
if ( this . webSocket . readyState !== WebSocket . OPEN ) {
this . emit ( 'error' , `Cannot send message: WebSocket is not open (state: ${ this . webSocket . readyState } )` );
return ;
}
try {
// Append newline to the text
const data = new TextEncoder (). encode ( text + '\n' );
this . webSocket . send ( data );
2025-07-25 15:11:02 +01:00
// Update stored session activity on send
this . updateStoredSessionActivity ();
2025-04-21 14:12:36 +02:00
// Emit the data for possible triggers
this . emit ( 'sent' , text );
} catch ( error ) {
console . error ( 'Error sending data:' , error );
2025-07-10 23:09:27 +01:00
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 ;
2025-07-25 15:11:02 +01:00
this . saveSessionToStorage ();
2025-07-10 23:09:27 +01:00
console . log ( 'Updated session ID:' , this . persistence . sessionId );
}
// Handle other system messages as needed
if ( systemData . type === 'session_resumed' ) {
console . log ( 'Session successfully resumed' );
2025-07-25 14:18:40 +01:00
if ( systemData . messagesReplayed > 0 ) {
console . log ( ` ${ systemData . messagesReplayed } messages were replayed` );
}
this . emit ( 'session_resumed' , systemData );
} else if ( systemData . type === 'message_replay_start' ) {
console . log ( `Starting message replay: ${ systemData . messageCount } messages from ${ systemData . timespan } ms ago` );
this . emit ( 'message_replay_start' , systemData );
} else if ( systemData . type === 'message_replay_complete' ) {
console . log ( 'Message replay complete' );
this . emit ( 'message_replay_complete' );
2025-07-10 23:09:27 +01:00
}
} catch ( error ) {
console . error ( 'Error parsing system message:' , error );
2025-04-21 14:12:36 +02:00
}
}
/**
* Disconnect from the MUD server
*/
public disconnect () : void {
2025-07-10 23:09:27 +01:00
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 );
}
}
2025-04-21 14:12:36 +02:00
if ( this . webSocket ) {
this . webSocket . close ();
this . webSocket = null ;
}
2025-07-10 23:09:27 +01:00
// Clear session ID since we're explicitly disconnecting
this . persistence . sessionId = undefined ;
this . persistence . reconnectAttempts = 0 ;
2025-07-25 15:11:02 +01:00
// Remove stored session from localStorage
this . clearStoredSession ();
2025-07-10 23:09:27 +01:00
// Clear reconnect timeout if active
if ( this . reconnectTimeoutId !== null ) {
clearTimeout ( this . reconnectTimeoutId );
this . reconnectTimeoutId = null ;
}
2025-04-21 14:12:36 +02:00
}
/**
* Handle incoming data from the MUD server
*/
private handleIncomingData ( data : Uint8Array ) : void {
// Quickly check if we need to handle telnet negotiation
let containsIAC = false ;
for ( let i = 0 ; i < data . length ; i ++ ) {
if ( data [ i ] === TelnetCommand . IAC ) {
containsIAC = true ;
break ;
}
}
2025-04-22 18:21:10 +02:00
// Debug: Log raw data for debugging if it contains IAC
if ( containsIAC ) {
const hexData = Array . from ( data ). map ( b => b . toString ( 16 ). padStart ( 2 , '0' )). join ( ' ' );
console . log ( `Raw data with IAC: ${ hexData } ` );
}
2025-04-21 14:12:36 +02:00
// Fast path if no IAC codes
if ( ! containsIAC && ! this . isInIAC ) {
const text = new TextDecoder (). decode ( data );
this . emit ( 'received' , text );
return ;
}
// Process each byte in the incoming data
for ( let i = 0 ; i < data . length ; i ++ ) {
const byte = data [ i ];
if ( this . isInIAC ) {
// Add byte to negotiation buffer
this . negotiationBuffer . push ( byte );
// Check for special sequences
if ( this . inSubnegotiation ) {
// Inside subnegotiation - look for IAC SE
if ( byte === TelnetCommand . SE &&
this . negotiationBuffer . length > 0 &&
this . negotiationBuffer [ this . negotiationBuffer . length - 2 ] === TelnetCommand . IAC ) {
2025-04-22 18:21:10 +02:00
console . log ( 'End of subnegotiation found' );
2025-04-21 14:12:36 +02:00
// Process the complete subnegotiation
this . handleCompleteSubnegotiation ();
// Reset state
this . isInIAC = false ;
this . inSubnegotiation = false ;
this . negotiationBuffer = [];
}
} else if ( this . negotiationBuffer . length === 2 ) {
// After IAC, check what command it is
if ( byte === TelnetCommand . SB ) {
// Start of subnegotiation
this . inSubnegotiation = true ;
} else if ( byte === TelnetCommand . WILL || byte === TelnetCommand . DO ) {
// Need one more byte for option
} else {
// Simple 3-byte command
this . processSimpleTelnetCommand ();
this . isInIAC = false ;
this . negotiationBuffer = [];
}
} else if ( this . negotiationBuffer . length === 3 && ! this . inSubnegotiation ) {
// Complete 3-byte command like IAC WILL X or IAC DO X
this . processSimpleTelnetCommand ();
this . isInIAC = false ;
this . negotiationBuffer = [];
}
} else if ( byte === TelnetCommand . IAC ) {
// Start of telnet command
this . isInIAC = true ;
this . negotiationBuffer = [ byte ];
2025-04-22 18:21:10 +02:00
console . log ( 'IAC command detected' );
2025-04-21 14:12:36 +02:00
} else {
// Normal data byte, add to buffer
this . buffer . push ( byte );
}
}
// Process any complete text in the buffer
if ( this . buffer . length > 0 ) {
const text = new TextDecoder (). decode ( new Uint8Array ( this . buffer ));
this . buffer = [];
// Emit the received text for display and trigger processing
this . emit ( 'received' , text );
}
}
/**
* Process a simple telnet command (3 bytes: IAC CMD OPTION)
*/
private processSimpleTelnetCommand () : void {
try {
const [ iac , command , option ] = this . negotiationBuffer ;
// Handle specific commands
if (( command === TelnetCommand . WILL || command === TelnetCommand . DO ) && option === TelnetCommand . GMCP ) {
2025-04-22 18:21:10 +02:00
console . log ( 'Server supports GMCP, responding with DO GMCP' );
2025-04-21 14:12:36 +02:00
// Server wants to use GMCP, we'll respond with IAC DO GMCP
this . sendIAC ( TelnetCommand . DO , TelnetCommand . GMCP );
2025-04-22 18:21:10 +02:00
// Request GMCP capabilities
console . log ( 'Requesting GMCP capabilities' );
this . gmcpHandler . requestCapabilities ();
2025-04-21 14:12:36 +02:00
}
} catch ( error ) {
console . error ( 'Error processing telnet command:' , error );
}
}
/**
* Handle a complete telnet subnegotiation sequence
*/
private handleCompleteSubnegotiation () : void {
try {
2025-04-22 18:21:10 +02:00
// Debug buffer contents
const bufferHex = this . negotiationBuffer . map ( b => b . toString ( 16 ). padStart ( 2 , '0' )). join ( ' ' );
console . log ( `Processing subnegotiation, buffer: ${ bufferHex } ` );
2025-04-21 14:12:36 +02:00
// Check if this is a GMCP subnegotiation
// IAC SB GMCP ... IAC SE
// Indexes: 0 1 2 ... -2 -1
if ( this . negotiationBuffer . length >= 5 && this . negotiationBuffer [ 2 ] === TelnetCommand . GMCP ) {
2025-04-22 18:21:10 +02:00
console . log ( 'Processing GMCP subnegotiation' );
2025-04-21 14:12:36 +02:00
try {
// Extract the GMCP data (skip IAC SB GMCP, and the final IAC SE)
const gmcpData = this . negotiationBuffer . slice ( 3 , - 2 );
const gmcpText = new TextDecoder (). decode ( new Uint8Array ( gmcpData ));
2025-04-22 18:21:10 +02:00
console . log ( `GMCP message: ${ gmcpText } ` );
2025-04-21 14:12:36 +02:00
2025-04-22 18:21:10 +02:00
// Process the GMCP message immediately
console . log ( 'Passing GMCP to handler:' , gmcpText );
this . gmcpHandler . handleGmcpMessage ( gmcpText );
2025-04-21 14:12:36 +02:00
} catch ( error ) {
2025-04-22 18:21:10 +02:00
console . error ( 'Error processing GMCP data:' , error );
2025-04-21 14:12:36 +02:00
}
} else {
2025-04-22 18:21:10 +02:00
console . log ( `Non-GMCP subnegotiation received: ${ this . negotiationBuffer [ 2 ] } ` );
2025-04-21 14:12:36 +02:00
}
} catch ( error ) {
console . error ( 'Error handling subnegotiation:' , error );
}
}
/**
* Send a telnet IAC sequence
*/
private sendIAC ( command : TelnetCommand , option : TelnetCommand ) : void {
if ( ! this . connected || ! this . webSocket ) {
return ;
}
const data = new Uint8Array ([ TelnetCommand . IAC , command , option ]);
this . webSocket . send ( data );
}
/**
* Send a GMCP message
*/
public sendGmcp ( module : string , data : any ) : void {
2025-04-22 18:21:10 +02:00
if ( ! this . connected || ! this . webSocket ) {
console . log ( 'Cannot send GMCP - not connected' );
2025-04-21 14:12:36 +02:00
return ;
}
2025-04-22 18:21:10 +02:00
console . log ( `Sending GMCP: ${ module } ` , data );
2025-04-21 14:12:36 +02:00
const gmcpString = ` ${ module } ${ JSON . stringify ( data ) } ` ;
const gmcpData = new TextEncoder (). encode ( gmcpString );
// Create the IAC SB GMCP <data> IAC SE sequence
const telnetSequence = new Uint8Array ([
TelnetCommand . IAC ,
TelnetCommand . SB ,
TelnetCommand . GMCP ,
... gmcpData ,
TelnetCommand . IAC ,
TelnetCommand . SE
]);
this . webSocket . send ( telnetSequence );
}
2025-04-22 18:21:10 +02:00
/**
* Get the GMCP handler associated with this connection
*/
public getGmcpHandler () : GmcpHandler {
return this . gmcpHandler ;
}
/**
* Check if the connection is active
*/
public isConnected () : boolean {
return this . connected ;
}
2025-07-10 23:09:27 +01:00
/**
* 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 ;
}
}
2025-07-25 15:11:02 +01:00
/**
* Save current session to localStorage
*/
private saveSessionToStorage () : void {
if ( ! this . persistence . sessionId ) {
return ;
}
const sessionData : StoredSessionData = {
sessionId : this.persistence.sessionId ,
profileId : this.id ,
host : this.host ,
port : this.port ,
useSSL : this.useSSL ,
lastActivity : Date.now (),
createdAt : Date.now ()
};
try {
const storageKey = `mudSession_ ${ this . id } ` ;
localStorage . setItem ( storageKey , JSON . stringify ( sessionData ));
console . log ( `Saved session ${ this . persistence . sessionId } to localStorage for profile ${ this . id } ` );
} catch ( error ) {
console . error ( 'Failed to save session to localStorage:' , error );
}
}
/**
* Load stored session from localStorage
*/
private loadStoredSession () : void {
try {
const storageKey = `mudSession_ ${ this . id } ` ;
const storedData = localStorage . getItem ( storageKey );
if ( ! storedData ) {
return ;
}
const sessionData : StoredSessionData = JSON . parse ( storedData );
// Validate that the stored session matches this connection
if ( sessionData . profileId === this . id &&
sessionData . host === this . host &&
sessionData . port === this . port &&
sessionData . useSSL === this . useSSL ) {
// Check if the session is still within a reasonable timeframe
const maxAge = 60 * 60 * 1000 ; // 1 hour max age
const age = Date . now () - sessionData . lastActivity ;
if ( age <= maxAge ) {
this . persistence . sessionId = sessionData . sessionId ;
console . log ( `Restored session ${ sessionData . sessionId } from localStorage for profile ${ this . id } (age: ${ Math . round ( age / 1000 ) } s)` );
} else {
console . log ( `Stored session for profile ${ this . id } is too old ( ${ Math . round ( age / 1000 ) } s), discarding` );
this . clearStoredSession ();
}
} else {
console . log ( `Stored session for profile ${ this . id } doesn't match current connection parameters, discarding` );
this . clearStoredSession ();
}
} catch ( error ) {
console . error ( 'Failed to load session from localStorage:' , error );
this . clearStoredSession ();
}
}
/**
* Clear stored session from localStorage
*/
private clearStoredSession () : void {
try {
const storageKey = `mudSession_ ${ this . id } ` ;
localStorage . removeItem ( storageKey );
console . log ( `Cleared stored session for profile ${ this . id } ` );
} catch ( error ) {
console . error ( 'Failed to clear stored session:' , error );
}
}
/**
* Update last activity timestamp in stored session
*/
private updateStoredSessionActivity () : void {
if ( ! this . persistence . sessionId ) {
return ;
}
try {
const storageKey = `mudSession_ ${ this . id } ` ;
const storedData = localStorage . getItem ( storageKey );
if ( storedData ) {
const sessionData : StoredSessionData = JSON . parse ( storedData );
sessionData . lastActivity = Date . now ();
localStorage . setItem ( storageKey , JSON . stringify ( sessionData ));
}
} catch ( error ) {
console . error ( 'Failed to update stored session activity:' , error );
}
}
/**
* Clean up old stored sessions from localStorage (static method)
*/
public static cleanupOldStoredSessions () : void {
try {
const maxAge = 60 * 60 * 1000 ; // 1 hour
const now = Date . now ();
const keysToRemove : string [] = [];
// Iterate through all localStorage keys
for ( let i = 0 ; i < localStorage . length ; i ++ ) {
const key = localStorage . key ( i );
if ( key && key . startsWith ( 'mudSession_' )) {
try {
const storedData = localStorage . getItem ( key );
if ( storedData ) {
const sessionData : StoredSessionData = JSON . parse ( storedData );
const age = now - sessionData . lastActivity ;
if ( age > maxAge ) {
keysToRemove . push ( key );
console . log ( `Marking old session for cleanup: ${ key } (age: ${ Math . round ( age / 1000 ) } s)` );
}
}
} catch ( error ) {
// If we can't parse the session data, remove it
keysToRemove . push ( key );
console . log ( `Marking corrupted session for cleanup: ${ key } ` );
}
}
}
// Remove old sessions
for ( const key of keysToRemove ) {
localStorage . removeItem ( key );
}
if ( keysToRemove . length > 0 ) {
console . log ( `Cleaned up ${ keysToRemove . length } old stored sessions` );
}
} catch ( error ) {
console . error ( 'Failed to cleanup old stored sessions:' , error );
}
}
2025-04-21 14:12:36 +02:00
}