More persistent connection stuff

This commit is contained in:
2025-07-25 15:11:02 +01:00
parent 5c9b4e151f
commit 060e9fa89a
2 changed files with 177 additions and 0 deletions

View File

@@ -31,6 +31,17 @@ interface ConnectionPersistence {
lastDisconnectTime?: number; 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 * MudConnection - Handles a single connection to a MUD server
* Each instance has its own GMCP handler and maintains its own state * 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 // Set up GMCP event forwarding
this.setupGmcpEvents(); this.setupGmcpEvents();
// Try to restore session from localStorage
this.loadStoredSession();
console.log(`MudConnection created for ${this.host}:${this.port} with ID ${this.id}`); 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}`); console.log(`Connected to ${this.host}:${this.port}`);
this.emit('connected'); this.emit('connected');
// Update stored session activity
this.updateStoredSessionActivity();
// Send GMCP negotiation upon connection // Send GMCP negotiation upon connection
console.log('Sending GMCP negotiation'); console.log('Sending GMCP negotiation');
this.sendIAC(TelnetCommand.WILL, TelnetCommand.GMCP); this.sendIAC(TelnetCommand.WILL, TelnetCommand.GMCP);
@@ -182,6 +199,7 @@ export class MudConnection extends EventEmitter {
} else { } else {
// Text data - let listeners process it directly // Text data - let listeners process it directly
// TriggerSystem will handle gagging and replacing in the component // TriggerSystem will handle gagging and replacing in the component
this.updateStoredSessionActivity();
this.emit('received', event.data); this.emit('received', event.data);
} }
} else if (event.data instanceof Blob) { } else if (event.data instanceof Blob) {
@@ -220,6 +238,9 @@ export class MudConnection extends EventEmitter {
const data = new TextEncoder().encode(text + '\n'); const data = new TextEncoder().encode(text + '\n');
this.webSocket.send(data); this.webSocket.send(data);
// Update stored session activity on send
this.updateStoredSessionActivity();
// Emit the data for possible triggers // Emit the data for possible triggers
this.emit('sent', text); this.emit('sent', text);
} catch (error) { } catch (error) {
@@ -243,6 +264,7 @@ export class MudConnection extends EventEmitter {
// Handle session ID updates // Handle session ID updates
if (systemData.sessionId) { if (systemData.sessionId) {
this.persistence.sessionId = systemData.sessionId; this.persistence.sessionId = systemData.sessionId;
this.saveSessionToStorage();
console.log('Updated session ID:', this.persistence.sessionId); console.log('Updated session ID:', this.persistence.sessionId);
} }
@@ -289,6 +311,9 @@ export class MudConnection extends EventEmitter {
this.persistence.sessionId = undefined; this.persistence.sessionId = undefined;
this.persistence.reconnectAttempts = 0; this.persistence.reconnectAttempts = 0;
// Remove stored session from localStorage
this.clearStoredSession();
// Clear reconnect timeout if active // Clear reconnect timeout if active
if (this.reconnectTimeoutId !== null) { if (this.reconnectTimeoutId !== null) {
clearTimeout(this.reconnectTimeoutId); clearTimeout(this.reconnectTimeoutId);
@@ -544,4 +569,152 @@ export class MudConnection extends EventEmitter {
this.reconnectTimeoutId = null; 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);
}
}
} }

View File

@@ -5,6 +5,7 @@
import { AccessibilityManager } from '$lib/accessibility/AccessibilityManager'; import { AccessibilityManager } from '$lib/accessibility/AccessibilityManager';
import { settingsManager } from '$lib/settings/SettingsManager'; import { settingsManager } from '$lib/settings/SettingsManager';
import { shortcutManager } from '$lib/utils/KeyboardShortcutManager'; import { shortcutManager } from '$lib/utils/KeyboardShortcutManager';
import { MudConnection } from '$lib/connection/MudConnection';
import PwaUpdater from '$lib/components/PwaUpdater.svelte'; import PwaUpdater from '$lib/components/PwaUpdater.svelte';
import '../app.css'; import '../app.css';
@@ -156,6 +157,9 @@
// Only execute browser-specific code in browser environment // Only execute browser-specific code in browser environment
if (!isBrowser) return; if (!isBrowser) return;
// Clean up old stored sessions on app startup
MudConnection.cleanupOldStoredSessions();
profileManager = new ProfileManager(); profileManager = new ProfileManager();
// Settings manager is initialized as a singleton when imported // Settings manager is initialized as a singleton when imported