Persistent connections even if client disconnects
This commit is contained in:
100
package-lock.json
generated
100
package-lock.json
generated
@@ -14,7 +14,6 @@
|
||||
"events": "^3.3.0",
|
||||
"express": "^4.18.2",
|
||||
"howler": "^2.2.4",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"net": "^1.0.2",
|
||||
"split.js": "^1.6.5",
|
||||
"ws": "^8.18.1"
|
||||
@@ -3199,15 +3198,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/http-proxy": {
|
||||
"version": "1.17.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz",
|
||||
"integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
|
||||
@@ -3707,6 +3697,7 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@@ -4959,12 +4950,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
@@ -5165,6 +5150,7 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -5223,26 +5209,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||
@@ -5690,44 +5656,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.0",
|
||||
"follow-redirects": "^1.0.0",
|
||||
"requires-port": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-middleware": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
|
||||
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-proxy": "^1.17.8",
|
||||
"http-proxy": "^1.18.1",
|
||||
"is-glob": "^4.0.1",
|
||||
"is-plain-obj": "^3.0.0",
|
||||
"micromatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/express": "^4.17.13"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/express": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/https-browserify": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
|
||||
@@ -6004,6 +5932,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -6058,6 +5987,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -6107,6 +6037,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@@ -6139,18 +6070,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-obj": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
|
||||
"integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-reference": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||
@@ -6638,6 +6557,7 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@@ -7216,6 +7136,7 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -7760,12 +7681,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
@@ -9068,6 +8983,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
|
||||
@@ -184,6 +184,11 @@
|
||||
// Listen for sound play events
|
||||
conn.on('playSound', handlePlaySound);
|
||||
|
||||
// Listen for message replay events
|
||||
conn.on('session_resumed', handleSessionResumed);
|
||||
conn.on('message_replay_start', handleMessageReplayStart);
|
||||
conn.on('message_replay_complete', handleMessageReplayComplete);
|
||||
|
||||
console.log('Connection listeners attached successfully');
|
||||
}
|
||||
|
||||
@@ -200,6 +205,9 @@
|
||||
conn.off('error', handleError);
|
||||
conn.off('gmcp', handleGmcp);
|
||||
conn.off('playSound', handlePlaySound);
|
||||
conn.off('session_resumed', handleSessionResumed);
|
||||
conn.off('message_replay_start', handleMessageReplayStart);
|
||||
conn.off('message_replay_complete', handleMessageReplayComplete);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -443,6 +451,44 @@
|
||||
console.log(`Play sound event for ${profileId}:`, options);
|
||||
dispatch('playSound', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle session resumed event
|
||||
*/
|
||||
function handleSessionResumed(data) {
|
||||
console.log(`Session resumed for ${profileId}:`, data);
|
||||
|
||||
if (data.messagesReplayed > 0) {
|
||||
// Add a system message to the output to notify the user
|
||||
addToOutputHistory(`[SYSTEM] Reconnected to MUD. ${data.messagesReplayed} messages have been replayed.`, false);
|
||||
} else {
|
||||
addToOutputHistory('[SYSTEM] Reconnected to MUD.', false);
|
||||
}
|
||||
|
||||
dispatch('sessionResumed', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle message replay start event
|
||||
*/
|
||||
function handleMessageReplayStart(data) {
|
||||
console.log(`Message replay starting for ${profileId}:`, data);
|
||||
|
||||
const timeAgo = Math.round(data.timespan / 1000);
|
||||
const timeUnit = timeAgo > 60 ? `${Math.round(timeAgo / 60)} minutes` : `${timeAgo} seconds`;
|
||||
|
||||
addToOutputHistory(`[SYSTEM] Replaying ${data.messageCount} messages from the last ${timeUnit}...`, false);
|
||||
dispatch('messageReplayStart', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle message replay complete event
|
||||
*/
|
||||
function handleMessageReplayComplete() {
|
||||
console.log(`Message replay complete for ${profileId}`);
|
||||
addToOutputHistory('[SYSTEM] Message replay complete. You are now up to date.', false);
|
||||
dispatch('messageReplayComplete');
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $connectionStatus[profileId]}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { uiSettings, accessibilitySettings } from '$lib/stores/mudStore';
|
||||
import { uiSettings, accessibilitySettings, connectionSettings } from '$lib/stores/mudStore';
|
||||
import { settingsManager } from '$lib/settings/SettingsManager';
|
||||
import BackupPanel from './BackupPanel.svelte';
|
||||
|
||||
@@ -146,6 +146,59 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Connection</h4>
|
||||
|
||||
<div class="setting-item">
|
||||
<span class="setting-name">Connection Persistence Timeout</span>
|
||||
<div class="range-control">
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="60"
|
||||
step="1"
|
||||
bind:value={$connectionSettings.persistenceTimeoutMinutes}
|
||||
>
|
||||
<span class="range-value">{$connectionSettings.persistenceTimeoutMinutes} min</span>
|
||||
</div>
|
||||
<div class="setting-description">
|
||||
How long to keep MUD connections alive when the app goes to the background (useful for mobile devices)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<span class="setting-name">Message Buffer Size</span>
|
||||
<div class="range-control">
|
||||
<input
|
||||
type="range"
|
||||
min="50"
|
||||
max="500"
|
||||
step="25"
|
||||
bind:value={$connectionSettings.maxBufferMessages}
|
||||
>
|
||||
<span class="range-value">{$connectionSettings.maxBufferMessages} messages</span>
|
||||
</div>
|
||||
<div class="setting-description">
|
||||
Maximum number of messages to buffer while disconnected for replay on reconnection
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<span class="setting-name">Buffer Memory Limit</span>
|
||||
<div class="range-control">
|
||||
<input
|
||||
type="range"
|
||||
min="5"
|
||||
max="50"
|
||||
step="5"
|
||||
bind:value={$connectionSettings.maxBufferSizeKB}
|
||||
>
|
||||
<span class="range-value">{$connectionSettings.maxBufferSizeKB} KB</span>
|
||||
</div>
|
||||
<div class="setting-description">
|
||||
Maximum memory to use for buffering messages (prevents excessive memory usage)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Debugging</h4>
|
||||
|
||||
<div class="setting-item">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { EventEmitter } from '$lib/utils/EventEmitter';
|
||||
import { GmcpHandler } from '$lib/gmcp/GmcpHandler';
|
||||
import { get } from 'svelte/store';
|
||||
import { connectionSettings } from '$lib/stores/mudStore';
|
||||
|
||||
// IAC codes for telnet negotiation
|
||||
enum TelnetCommand {
|
||||
@@ -130,6 +132,12 @@ export class MudConnection extends EventEmitter {
|
||||
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}`;
|
||||
|
||||
console.log(`Connecting to WebSocket server: ${wsUrl}`);
|
||||
|
||||
this.webSocket = new WebSocket(wsUrl);
|
||||
@@ -241,7 +249,16 @@ export class MudConnection extends EventEmitter {
|
||||
// Handle other system messages as needed
|
||||
if (systemData.type === 'session_resumed') {
|
||||
console.log('Session successfully resumed');
|
||||
this.emit('session_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');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing system message:', error);
|
||||
|
||||
@@ -25,6 +25,11 @@ export interface Settings {
|
||||
debugGmcp: boolean;
|
||||
globalVolume: number;
|
||||
};
|
||||
connection: {
|
||||
persistenceTimeoutMinutes: number;
|
||||
maxBufferMessages: number;
|
||||
maxBufferSizeKB: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class SettingsManager extends EventEmitter {
|
||||
@@ -35,6 +40,7 @@ export class SettingsManager extends EventEmitter {
|
||||
// Create our own stores rather than depending on the ones from mudStore
|
||||
public accessibilitySettings: Writable<Settings['accessibility']>;
|
||||
public uiSettings: Writable<Settings['ui']>;
|
||||
public connectionSettings: Writable<Settings['connection']>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -63,12 +69,18 @@ export class SettingsManager extends EventEmitter {
|
||||
font: 'monospace',
|
||||
debugGmcp: false,
|
||||
globalVolume: 0.7
|
||||
},
|
||||
connection: {
|
||||
persistenceTimeoutMinutes: 5,
|
||||
maxBufferMessages: 100,
|
||||
maxBufferSizeKB: 10
|
||||
}
|
||||
};
|
||||
|
||||
// Create the stores with default values
|
||||
this.accessibilitySettings = writable(this.settings.accessibility);
|
||||
this.uiSettings = writable(this.settings.ui);
|
||||
this.connectionSettings = writable(this.settings.connection);
|
||||
|
||||
// Set up subscriptions to save settings when they change
|
||||
this.accessibilitySettings.subscribe(value => {
|
||||
@@ -91,6 +103,16 @@ export class SettingsManager extends EventEmitter {
|
||||
}
|
||||
});
|
||||
|
||||
this.connectionSettings.subscribe(value => {
|
||||
// Skip during initialization
|
||||
if (!this.initialized) return;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// Use a small timeout to batch multiple rapid changes
|
||||
setTimeout(() => this.saveSettings(), 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Load settings from storage
|
||||
this.loadSettings();
|
||||
}
|
||||
@@ -123,6 +145,10 @@ export class SettingsManager extends EventEmitter {
|
||||
ui: {
|
||||
...this.settings.ui,
|
||||
...(parsedSettings.ui || {})
|
||||
},
|
||||
connection: {
|
||||
...this.settings.connection,
|
||||
...(parsedSettings.connection || {})
|
||||
}
|
||||
};
|
||||
|
||||
@@ -131,6 +157,7 @@ export class SettingsManager extends EventEmitter {
|
||||
// Set the stores without triggering the save callback
|
||||
this.accessibilitySettings.set(this.settings.accessibility);
|
||||
this.uiSettings.set(this.settings.ui);
|
||||
this.connectionSettings.set(this.settings.connection);
|
||||
} else {
|
||||
console.warn('Invalid settings format found in localStorage');
|
||||
}
|
||||
@@ -158,6 +185,7 @@ export class SettingsManager extends EventEmitter {
|
||||
// Get current values from stores
|
||||
this.settings.accessibility = get(this.accessibilitySettings);
|
||||
this.settings.ui = get(this.uiSettings);
|
||||
this.settings.connection = get(this.connectionSettings);
|
||||
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.settings));
|
||||
console.log('Saved settings to localStorage');
|
||||
@@ -177,6 +205,10 @@ export class SettingsManager extends EventEmitter {
|
||||
this.uiSettings.update(current => ({...current, ...newSettings}));
|
||||
}
|
||||
|
||||
public updateConnectionSettings(newSettings: Partial<Settings['connection']>): void {
|
||||
this.connectionSettings.update(current => ({...current, ...newSettings}));
|
||||
}
|
||||
|
||||
// Reset settings to defaults
|
||||
public resetSettings(): void {
|
||||
const defaults = {
|
||||
@@ -202,11 +234,17 @@ export class SettingsManager extends EventEmitter {
|
||||
font: 'monospace',
|
||||
debugGmcp: false,
|
||||
globalVolume: 0.7
|
||||
},
|
||||
connection: {
|
||||
persistenceTimeoutMinutes: 5,
|
||||
maxBufferMessages: 100,
|
||||
maxBufferSizeKB: 10
|
||||
}
|
||||
};
|
||||
|
||||
this.accessibilitySettings.set(defaults.accessibility);
|
||||
this.uiSettings.set(defaults.ui);
|
||||
this.connectionSettings.set(defaults.connection);
|
||||
this.emit('settingsReset', defaults);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export const connectionStatus = writable<{ [key: string]: 'connected' | 'disconn
|
||||
// Use the stores from SettingsManager
|
||||
export const accessibilitySettings = settingsManager.accessibilitySettings;
|
||||
export const uiSettings = settingsManager.uiSettings;
|
||||
export const connectionSettings = settingsManager.connectionSettings;
|
||||
|
||||
// Store for input history - keyed by profile ID
|
||||
export const inputHistory = writable<{ [profileId: string]: string[] }>({});
|
||||
|
||||
@@ -4,8 +4,11 @@ import * as tls from 'tls';
|
||||
import http from 'http';
|
||||
import { parse } from 'url';
|
||||
|
||||
// Configuration for connection persistence
|
||||
const CONNECTION_PERSISTENCE_TIMEOUT = 5 * 60 * 1000; // 5 minutes in milliseconds
|
||||
// Default configuration for connection persistence (fallback values)
|
||||
const DEFAULT_PERSISTENCE_TIMEOUT = 5 * 60 * 1000; // 5 minutes in milliseconds
|
||||
const DEFAULT_MAX_BUFFER_MESSAGES = 100; // Maximum number of messages to buffer
|
||||
const DEFAULT_MAX_BUFFER_SIZE_KB = 10; // Maximum buffer size in KB
|
||||
|
||||
const HEARTBEAT_INTERVAL = 30 * 1000; // 30 seconds
|
||||
|
||||
// Create HTTP server
|
||||
@@ -15,17 +18,113 @@ const server = http.createServer();
|
||||
const wss = new WebSocketServer({ noServer: true });
|
||||
|
||||
// Active connections and their proxies
|
||||
// Key: connectionId, Value: { ws, socket, sessionId, settings }
|
||||
const connections = new Map();
|
||||
|
||||
// Persistent connections waiting for reconnection
|
||||
// Key: sessionId, Value: { socket, mudHost, mudPort, useSSL, timeoutId, lastActivity }
|
||||
// Key: sessionId, Value: { socket, mudHost, mudPort, useSSL, timeoutId, lastActivity, messageBuffer, settings }
|
||||
const persistentConnections = new Map();
|
||||
|
||||
// Parse connection settings from URL parameters with defaults
|
||||
function parseConnectionSettings(urlParts) {
|
||||
const persistenceTimeoutParam = urlParts.searchParams.get('persistenceTimeout');
|
||||
const maxBufferMessagesParam = urlParts.searchParams.get('maxBufferMessages');
|
||||
const maxBufferSizeKBParam = urlParts.searchParams.get('maxBufferSizeKB');
|
||||
|
||||
return {
|
||||
persistenceTimeoutMs: persistenceTimeoutParam ?
|
||||
parseInt(persistenceTimeoutParam) * 60 * 1000 : // Convert minutes to milliseconds
|
||||
DEFAULT_PERSISTENCE_TIMEOUT,
|
||||
maxBufferMessages: maxBufferMessagesParam ?
|
||||
parseInt(maxBufferMessagesParam) :
|
||||
DEFAULT_MAX_BUFFER_MESSAGES,
|
||||
maxBufferSizeKB: maxBufferSizeKBParam ?
|
||||
parseInt(maxBufferSizeKBParam) :
|
||||
DEFAULT_MAX_BUFFER_SIZE_KB
|
||||
};
|
||||
}
|
||||
|
||||
// Generate a unique session ID for persistent connections
|
||||
function generateSessionId() {
|
||||
return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
// Buffer a message for a persistent connection
|
||||
function bufferMessage(sessionId, data) {
|
||||
const persistentConn = persistentConnections.get(sessionId);
|
||||
if (!persistentConn) {
|
||||
return; // No persistent connection to buffer for
|
||||
}
|
||||
|
||||
if (!persistentConn.messageBuffer) {
|
||||
persistentConn.messageBuffer = [];
|
||||
persistentConn.bufferSize = 0;
|
||||
}
|
||||
|
||||
// Add timestamp to the message
|
||||
const bufferedMessage = {
|
||||
data: data,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
persistentConn.messageBuffer.push(bufferedMessage);
|
||||
persistentConn.bufferSize += data.length;
|
||||
|
||||
// Use this connection's specific settings for buffer limits
|
||||
const settings = persistentConn.settings || {
|
||||
maxBufferMessages: DEFAULT_MAX_BUFFER_MESSAGES,
|
||||
maxBufferSizeKB: DEFAULT_MAX_BUFFER_SIZE_KB
|
||||
};
|
||||
|
||||
// Trim buffer if it gets too large
|
||||
while (persistentConn.messageBuffer.length > settings.maxBufferMessages ||
|
||||
persistentConn.bufferSize > settings.maxBufferSizeKB * 1000) {
|
||||
const removed = persistentConn.messageBuffer.shift();
|
||||
if (removed) {
|
||||
persistentConn.bufferSize -= removed.data.length;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Buffered ${data.length} bytes for session ${sessionId} (${persistentConn.messageBuffer.length} messages, ${persistentConn.bufferSize} bytes total, limits: ${settings.maxBufferMessages} msgs/${settings.maxBufferSizeKB}KB)`);
|
||||
}
|
||||
|
||||
// Replay buffered messages to a reconnected client
|
||||
function replayBufferedMessages(ws, sessionId) {
|
||||
const persistentConn = persistentConnections.get(sessionId);
|
||||
if (!persistentConn || !persistentConn.messageBuffer) {
|
||||
return 0; // No messages to replay
|
||||
}
|
||||
|
||||
const messages = persistentConn.messageBuffer;
|
||||
console.log(`Replaying ${messages.length} buffered messages for session ${sessionId}`);
|
||||
|
||||
// Send a notification about message replay
|
||||
const replayNotification = `[SYSTEM]${JSON.stringify({
|
||||
type: 'message_replay_start',
|
||||
messageCount: messages.length,
|
||||
timespan: messages.length > 0 ? Date.now() - messages[0].timestamp : 0
|
||||
})}`;
|
||||
ws.send(replayNotification);
|
||||
|
||||
// Send all buffered messages
|
||||
for (const message of messages) {
|
||||
if (ws.readyState === 1) { // WebSocket.OPEN
|
||||
ws.send(message.data);
|
||||
}
|
||||
}
|
||||
|
||||
// Send replay complete notification
|
||||
const replayComplete = `[SYSTEM]${JSON.stringify({ type: 'message_replay_complete' })}`;
|
||||
ws.send(replayComplete);
|
||||
|
||||
// Clear the buffer since messages have been replayed
|
||||
const messageCount = messages.length;
|
||||
persistentConn.messageBuffer = [];
|
||||
persistentConn.bufferSize = 0;
|
||||
|
||||
return messageCount;
|
||||
}
|
||||
|
||||
// Clean up a persistent connection
|
||||
function cleanupPersistentConnection(sessionId) {
|
||||
const persistentConn = persistentConnections.get(sessionId);
|
||||
@@ -54,11 +153,16 @@ wss.on('connection', (ws, req, mudHost, mudPort, useSSL) => {
|
||||
// Create a unique ID for this connection
|
||||
const connectionId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// Check for session ID in query parameters for reconnection
|
||||
// Check for session ID and settings in query parameters
|
||||
const url = req.url || '';
|
||||
const urlParts = new URL(`http://localhost${url}`);
|
||||
const sessionId = urlParts.searchParams.get('sessionId');
|
||||
|
||||
// Parse connection settings for this specific connection
|
||||
const connectionSettings = parseConnectionSettings(urlParts);
|
||||
|
||||
console.log(`Connection settings for ${connectionId}: timeout=${connectionSettings.persistenceTimeoutMs/1000/60}min, maxMessages=${connectionSettings.maxBufferMessages}, maxSizeKB=${connectionSettings.maxBufferSizeKB}`);
|
||||
|
||||
// Special handling for test connections
|
||||
if (mudHost === 'example.com' && mudPort === '23') {
|
||||
console.log('Test connection detected - using echo server mode');
|
||||
@@ -98,11 +202,18 @@ wss.on('connection', (ws, req, mudHost, mudPort, useSSL) => {
|
||||
clearTimeout(persistentConn.timeoutId);
|
||||
}
|
||||
|
||||
// Remove from persistent connections (now active again)
|
||||
// Replay any buffered messages first
|
||||
const replayedCount = replayBufferedMessages(ws, sessionId);
|
||||
|
||||
// Remove from persistent connections (now active again) - do this after replay
|
||||
persistentConnections.delete(sessionId);
|
||||
|
||||
// Send reconnection notification with session ID in proper JSON format
|
||||
ws.send(`[SYSTEM]${JSON.stringify({ type: 'session_resumed', sessionId: sessionId })}`);
|
||||
ws.send(`[SYSTEM]${JSON.stringify({
|
||||
type: 'session_resumed',
|
||||
sessionId: sessionId,
|
||||
messagesReplayed: replayedCount
|
||||
})}`);
|
||||
} else {
|
||||
// Create new connection
|
||||
currentSessionId = generateSessionId();
|
||||
@@ -142,8 +253,13 @@ wss.on('connection', (ws, req, mudHost, mudPort, useSSL) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Store the connection
|
||||
connections.set(connectionId, { ws, socket, sessionId: currentSessionId });
|
||||
// Store the connection with its settings
|
||||
connections.set(connectionId, {
|
||||
ws,
|
||||
socket,
|
||||
sessionId: currentSessionId,
|
||||
settings: connectionSettings
|
||||
});
|
||||
|
||||
// Handle data from the MUD server - only in regular mode, not test mode
|
||||
if (socket) {
|
||||
@@ -163,6 +279,11 @@ wss.on('connection', (ws, req, mudHost, mudPort, useSSL) => {
|
||||
if (ws.readyState === 1) { // WebSocket.OPEN
|
||||
ws.send(data);
|
||||
console.log(`WebSocket server: Sent ${data.length} bytes to client${isGmcp ? ' (contains GMCP data)' : ''}`);
|
||||
} else {
|
||||
// WebSocket is not open, buffer the message if we have a session
|
||||
if (currentSessionId) {
|
||||
bufferMessage(currentSessionId, data);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -276,13 +397,14 @@ wss.on('connection', (ws, req, mudHost, mudPort, useSSL) => {
|
||||
|
||||
const conn = connections.get(connectionId);
|
||||
if (conn && !conn.testMode && conn.socket && !conn.socket.destroyed) {
|
||||
console.log(`Moving connection to persistent state for ${CONNECTION_PERSISTENCE_TIMEOUT / 1000} seconds`);
|
||||
console.log(`Moving connection to persistent state for ${conn.settings.persistenceTimeoutMs / 1000} seconds`);
|
||||
|
||||
// Move the connection to persistent storage instead of closing it
|
||||
// Use this connection's specific timeout setting
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.log(`Session ${currentSessionId} timed out, closing MUD connection`);
|
||||
cleanupPersistentConnection(currentSessionId);
|
||||
}, CONNECTION_PERSISTENCE_TIMEOUT);
|
||||
}, conn.settings.persistenceTimeoutMs);
|
||||
|
||||
persistentConnections.set(currentSessionId, {
|
||||
socket: conn.socket,
|
||||
@@ -290,10 +412,13 @@ wss.on('connection', (ws, req, mudHost, mudPort, useSSL) => {
|
||||
mudPort,
|
||||
useSSL,
|
||||
timeoutId,
|
||||
lastActivity: Date.now()
|
||||
lastActivity: Date.now(),
|
||||
messageBuffer: [],
|
||||
bufferSize: 0,
|
||||
settings: conn.settings // Store the connection's settings
|
||||
});
|
||||
|
||||
console.log(`Session ${currentSessionId} will persist for ${CONNECTION_PERSISTENCE_TIMEOUT / 1000} seconds`);
|
||||
console.log(`Session ${currentSessionId} will persist for ${conn.settings.persistenceTimeoutMs / 1000} seconds with settings: ${conn.settings.maxBufferMessages} msgs/${conn.settings.maxBufferSizeKB}KB`);
|
||||
} else if (conn && conn.socket) {
|
||||
// Fallback to immediate cleanup if needed
|
||||
conn.socket.end();
|
||||
@@ -347,18 +472,20 @@ setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [sessionId, persistentConn] of persistentConnections.entries()) {
|
||||
// Clean up connections that have been inactive for too long
|
||||
if (now - persistentConn.lastActivity > CONNECTION_PERSISTENCE_TIMEOUT * 2) {
|
||||
// Use double the connection's specific timeout or default
|
||||
const timeoutThreshold = (persistentConn.settings?.persistenceTimeoutMs || DEFAULT_PERSISTENCE_TIMEOUT) * 2;
|
||||
if (now - persistentConn.lastActivity > timeoutThreshold) {
|
||||
console.log(`Cleaning up abandoned session: ${sessionId}`);
|
||||
cleanupPersistentConnection(sessionId);
|
||||
}
|
||||
}
|
||||
}, CONNECTION_PERSISTENCE_TIMEOUT);
|
||||
}, DEFAULT_PERSISTENCE_TIMEOUT); // Run cleanup every default timeout period
|
||||
|
||||
// Start the WebSocket server
|
||||
const PORT = process.env.WS_PORT || 3001;
|
||||
server.listen(PORT, () => {
|
||||
console.log(`WebSocket server is running on port ${PORT}`);
|
||||
console.log(`Connection persistence timeout: ${CONNECTION_PERSISTENCE_TIMEOUT / 1000} seconds`);
|
||||
console.log(`Default connection persistence timeout: ${DEFAULT_PERSISTENCE_TIMEOUT / 1000} seconds (configurable per connection)`);
|
||||
});
|
||||
|
||||
export default server;
|
||||
Reference in New Issue
Block a user