From 060e9fa89ac999b09f1f35f43dd118fefc0503f2 Mon Sep 17 00:00:00 2001 From: Talon Date: Fri, 25 Jul 2025 15:11:02 +0100 Subject: [PATCH] More persistent connection stuff --- src/lib/connection/MudConnection.ts | 173 ++++++++++++++++++++++++++++ src/routes/+layout.svelte | 4 + 2 files changed, 177 insertions(+) diff --git a/src/lib/connection/MudConnection.ts b/src/lib/connection/MudConnection.ts index aa377ec..1d2c4ff 100644 --- a/src/lib/connection/MudConnection.ts +++ b/src/lib/connection/MudConnection.ts @@ -31,6 +31,17 @@ interface ConnectionPersistence { lastDisconnectTime?: number; } +// Stored session data in localStorage +interface StoredSessionData { + sessionId: string; + profileId: string; + host: string; + port: number; + useSSL: boolean; + lastActivity: number; + createdAt: number; +} + /** * MudConnection - Handles a single connection to a MUD server * Each instance has its own GMCP handler and maintains its own state @@ -71,6 +82,9 @@ export class MudConnection extends EventEmitter { // Set up GMCP event forwarding this.setupGmcpEvents(); + // Try to restore session from localStorage + this.loadStoredSession(); + console.log(`MudConnection created for ${this.host}:${this.port} with ID ${this.id}`); } @@ -149,6 +163,9 @@ export class MudConnection extends EventEmitter { console.log(`Connected to ${this.host}:${this.port}`); this.emit('connected'); + // Update stored session activity + this.updateStoredSessionActivity(); + // Send GMCP negotiation upon connection console.log('Sending GMCP negotiation'); this.sendIAC(TelnetCommand.WILL, TelnetCommand.GMCP); @@ -182,6 +199,7 @@ export class MudConnection extends EventEmitter { } else { // Text data - let listeners process it directly // TriggerSystem will handle gagging and replacing in the component + this.updateStoredSessionActivity(); this.emit('received', event.data); } } else if (event.data instanceof Blob) { @@ -220,6 +238,9 @@ export class MudConnection extends EventEmitter { const data = new TextEncoder().encode(text + '\n'); this.webSocket.send(data); + // Update stored session activity on send + this.updateStoredSessionActivity(); + // Emit the data for possible triggers this.emit('sent', text); } catch (error) { @@ -243,6 +264,7 @@ export class MudConnection extends EventEmitter { // Handle session ID updates if (systemData.sessionId) { this.persistence.sessionId = systemData.sessionId; + this.saveSessionToStorage(); console.log('Updated session ID:', this.persistence.sessionId); } @@ -289,6 +311,9 @@ export class MudConnection extends EventEmitter { this.persistence.sessionId = undefined; this.persistence.reconnectAttempts = 0; + // Remove stored session from localStorage + this.clearStoredSession(); + // Clear reconnect timeout if active if (this.reconnectTimeoutId !== null) { clearTimeout(this.reconnectTimeoutId); @@ -544,4 +569,152 @@ export class MudConnection extends EventEmitter { this.reconnectTimeoutId = null; } } + + /** + * 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); + } + } } \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 3abaedb..ad1b26d 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -5,6 +5,7 @@ import { AccessibilityManager } from '$lib/accessibility/AccessibilityManager'; import { settingsManager } from '$lib/settings/SettingsManager'; import { shortcutManager } from '$lib/utils/KeyboardShortcutManager'; + import { MudConnection } from '$lib/connection/MudConnection'; import PwaUpdater from '$lib/components/PwaUpdater.svelte'; import '../app.css'; @@ -156,6 +157,9 @@ // Only execute browser-specific code in browser environment if (!isBrowser) return; + // Clean up old stored sessions on app startup + MudConnection.cleanupOldStoredSessions(); + profileManager = new ProfileManager(); // Settings manager is initialized as a singleton when imported