Files
svelte-mud/src/lib/connection/MudConnection.ts
T

720 lines
23 KiB
TypeScript
Raw Normal View History

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';
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}`);
}
// 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');
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
}