Multi-Server
Server switching and room isolation
Gryt supports connecting to multiple servers simultaneously with seamless switching and isolated voice channels.
Key Features
Server Discovery
- Automatic Detection: Automatic server detection and connection
- Manual Addition: Add custom servers through the interface
- Server Validation: Verify server availability and compatibility
- Connection Testing: Test connectivity before joining
Seamless Server Switching
- No Disconnection: Switch between servers without losing voice connection
- State Preservation: Maintain user state across server switches
- Background Connection: Keep connections alive to multiple servers
- Smart Routing: Intelligent connection management
Room Isolation
- Unique Room IDs: Server-prefixed room IDs prevent cross-server interference
- Independent Channels: Each server maintains its own voice channels
- User State Separation: User states are isolated per server
- Resource Management: Efficient resource allocation per server
Connection State Management
- Robust Handling: Handles network changes and reconnections
- Automatic Recovery: Automatic reconnection with exponential backoff
- State Synchronization: Real-time state updates across all servers
- Error Handling: Graceful handling of connection failures
Architecture Overview
The multi-server architecture consists of several key components:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Server A │ │ Server B │ │ Server C │
│ │ │ │ │ │
│ • Room: tech_1 │ │ • Room: game_1 │ │ • Room: music_1 │
│ • Users: 5 │ │ • Users: 12 │ │ • Users: 3 │
│ • Status: ✅ │ │ • Status: ✅ │ │ • Status: ⚠️ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└───────────────────────┼───────────────────────┘
│
┌─────────────────┐
│ Web Client │
│ │
│ • Multi-Server │
│ Management │
│ • State Sync │
│ • Connection │
│ Pool │
└─────────────────┘Server Management
Server Discovery
Automatic and manual server discovery:
// Server discovery interface
interface ServerInfo {
id: string;
name: string;
url: string;
icon?: string;
status: 'online' | 'offline' | 'connecting' | 'error';
users: number;
channels: Channel[];
lastSeen: Date;
}
// Server discovery methods
const serverDiscovery = {
// Automatic discovery via mDNS/Bonjour
discoverLocalServers: async (): Promise<ServerInfo[]> => {
// Implementation for local network discovery
return await mDNSDiscovery();
},
// Manual server addition
addCustomServer: async (url: string): Promise<ServerInfo> => {
const serverInfo = await validateServer(url);
if (serverInfo) {
await saveServer(serverInfo);
return serverInfo;
}
throw new Error('Invalid server URL');
},
// Server validation
validateServer: async (url: string): Promise<boolean> => {
try {
const response = await fetch(`${url}/health`);
return response.ok;
} catch {
return false;
}
}
};Server Connection Management
// Multi-server connection manager
class MultiServerManager {
private servers: Map<string, ServerConnection> = new Map();
private currentServer: string | null = null;
// Connect to a server
async connectToServer(serverId: string): Promise<void> {
const server = this.servers.get(serverId);
if (!server) {
throw new Error('Server not found');
}
// Establish WebSocket connection
await server.connect();
// Update connection state
this.updateServerStatus(serverId, 'connected');
}
// Switch to a different server
async switchServer(serverId: string): Promise<void> {
if (this.currentServer === serverId) {
return; // Already connected to this server
}
// Disconnect from current server (but keep connection alive)
if (this.currentServer) {
await this.disconnectFromCurrentServer();
}
// Connect to new server
await this.connectToServer(serverId);
this.currentServer = serverId;
// Notify UI of server switch
this.emit('server-switched', serverId);
}
// Keep connections alive to all servers
private keepConnectionsAlive(): void {
this.servers.forEach(server => {
if (server.status === 'connected') {
server.ping();
}
});
}
}Room Isolation
Unique Room ID Generation
Each server maintains its own room namespace:
// Room ID generation with server prefixing
const generateRoomId = (serverName: string, channelId: string): string => {
// Extract server prefix (e.g., "techial" from "techial.example.com")
const prefix = serverName.split('.')[0].toLowerCase();
// Generate unique room ID
const roomId = `${prefix}_${channelId}`;
return roomId;
};
// Example room IDs
const roomIds = {
'techial.example.com': {
'general': 'techial_general',
'voice': 'techial_voice',
'gaming': 'techial_gaming'
},
'gryta.example.com': {
'general': 'gryta_general',
'voice': 'gryta_voice',
'music': 'gryta_music'
}
};Room State Management
// Room state management per server
interface RoomState {
serverId: string;
roomId: string;
users: Map<string, UserState>;
isActive: boolean;
lastActivity: Date;
}
// Multi-server room manager
class MultiServerRoomManager {
private rooms: Map<string, RoomState> = new Map();
// Join a room on a specific server
async joinRoom(serverId: string, channelId: string): Promise<void> {
const server = this.getServer(serverId);
const roomId = generateRoomId(server.name, channelId);
// Create room state if it doesn't exist
if (!this.rooms.has(roomId)) {
this.rooms.set(roomId, {
serverId,
roomId,
users: new Map(),
isActive: false,
lastActivity: new Date()
});
}
// Join the room
await server.joinRoom(roomId);
// Update room state
const room = this.rooms.get(roomId)!;
room.isActive = true;
room.lastActivity = new Date();
}
// Leave a room
async leaveRoom(serverId: string, channelId: string): Promise<void> {
const server = this.getServer(serverId);
const roomId = generateRoomId(server.name, channelId);
// Leave the room
await server.leaveRoom(roomId);
// Update room state
const room = this.rooms.get(roomId);
if (room) {
room.isActive = false;
room.lastActivity = new Date();
}
}
}Seamless Server Switching
State Preservation
Maintain user state across server switches:
// User state preservation
interface UserState {
id: string;
nickname: string;
isMuted: boolean;
isDeafened: boolean;
microphoneVolume: number;
outputVolume: number;
audioDevice: string;
preferences: UserPreferences;
}
// State manager for multi-server
class MultiServerStateManager {
private globalState: UserState;
private serverStates: Map<string, Partial<UserState>> = new Map();
// Save state before server switch
saveStateForServer(serverId: string): void {
this.serverStates.set(serverId, {
isMuted: this.globalState.isMuted,
microphoneVolume: this.globalState.microphoneVolume,
outputVolume: this.globalState.outputVolume
});
}
// Restore state after server switch
restoreStateForServer(serverId: string): void {
const serverState = this.serverStates.get(serverId);
if (serverState) {
// Restore server-specific state
Object.assign(this.globalState, serverState);
// Apply state to audio pipeline
this.applyAudioState(this.globalState);
}
}
// Apply audio state
private applyAudioState(state: UserState): void {
// Update audio pipeline
updateAudioPipeline({
muted: state.isMuted,
microphoneVolume: state.microphoneVolume,
outputVolume: state.outputVolume
});
}
}Background Connection Management
// Background connection manager
class BackgroundConnectionManager {
private connections: Map<string, WebSocket> = new Map();
private keepAliveInterval: NodeJS.Timeout;
// Maintain background connections
maintainConnections(): void {
this.keepAliveInterval = setInterval(() => {
this.connections.forEach((ws, serverId) => {
if (ws.readyState === WebSocket.OPEN) {
// Send ping to keep connection alive
ws.send(JSON.stringify({ type: 'ping' }));
} else if (ws.readyState === WebSocket.CLOSED) {
// Attempt to reconnect
this.reconnect(serverId);
}
});
}, 30000); // Ping every 30 seconds
}
// Reconnect to a server
private async reconnect(serverId: string): Promise<void> {
try {
const server = this.getServer(serverId);
const ws = new WebSocket(server.url);
ws.onopen = () => {
this.connections.set(serverId, ws);
this.emit('server-reconnected', serverId);
};
ws.onclose = () => {
this.connections.delete(serverId);
this.emit('server-disconnected', serverId);
};
} catch (error) {
console.error(`Failed to reconnect to server ${serverId}:`, error);
}
}
}Connection State Management
Robust Error Handling
// Connection state management
interface ConnectionState {
status: 'connecting' | 'connected' | 'disconnected' | 'error';
lastError?: string;
reconnectAttempts: number;
maxReconnectAttempts: number;
reconnectDelay: number;
}
// Connection manager with exponential backoff
class ConnectionManager {
private states: Map<string, ConnectionState> = new Map();
// Handle connection errors
handleConnectionError(serverId: string, error: Error): void {
const state = this.states.get(serverId) || this.createDefaultState();
state.status = 'error';
state.lastError = error.message;
state.reconnectAttempts++;
this.states.set(serverId, state);
// Attempt reconnection with exponential backoff
if (state.reconnectAttempts < state.maxReconnectAttempts) {
const delay = Math.min(1000 * Math.pow(2, state.reconnectAttempts), 30000);
setTimeout(() => this.reconnect(serverId), delay);
}
}
// Reconnect with exponential backoff
private async reconnect(serverId: string): Promise<void> {
const state = this.states.get(serverId)!;
try {
state.status = 'connecting';
await this.connectToServer(serverId);
// Reset state on successful connection
state.status = 'connected';
state.reconnectAttempts = 0;
state.lastError = undefined;
} catch (error) {
this.handleConnectionError(serverId, error as Error);
}
}
}State Synchronization
// State synchronization across servers
class StateSynchronizer {
private syncQueue: Map<string, any[]> = new Map();
// Synchronize state across all connected servers
async syncState(state: Partial<UserState>): Promise<void> {
const promises = Array.from(this.connectedServers).map(serverId =>
this.syncStateToServer(serverId, state)
);
await Promise.allSettled(promises);
}
// Sync state to a specific server
private async syncStateToServer(serverId: string, state: Partial<UserState>): Promise<void> {
try {
const server = this.getServer(serverId);
await server.updateUserState(state);
} catch (error) {
// Queue for retry
this.queueForRetry(serverId, state);
}
}
// Queue failed syncs for retry
private queueForRetry(serverId: string, state: Partial<UserState>): void {
if (!this.syncQueue.has(serverId)) {
this.syncQueue.set(serverId, []);
}
this.syncQueue.get(serverId)!.push(state);
}
}User Interface
Server List Management
// Server list UI component
const ServerList = () => {
const [servers, setServers] = useState<ServerInfo[]>([]);
const [currentServer, setCurrentServer] = useState<string | null>(null);
// Add new server
const addServer = async (url: string) => {
try {
const serverInfo = await serverDiscovery.addCustomServer(url);
setServers(prev => [...prev, serverInfo]);
} catch (error) {
showError('Failed to add server: ' + error.message);
}
};
// Switch to server
const switchToServer = async (serverId: string) => {
try {
await multiServerManager.switchServer(serverId);
setCurrentServer(serverId);
} catch (error) {
showError('Failed to switch server: ' + error.message);
}
};
return (
<div className="server-list">
{servers.map(server => (
<ServerItem
key={server.id}
server={server}
isActive={currentServer === server.id}
onSwitch={() => switchToServer(server.id)}
onRemove={() => removeServer(server.id)}
/>
))}
<AddServerButton onAdd={addServer} />
</div>
);
};Server Status Indicators
// Server status indicator component
const ServerStatusIndicator = ({ server }: { server: ServerInfo }) => {
const getStatusColor = (status: string) => {
switch (status) {
case 'online': return 'green';
case 'offline': return 'red';
case 'connecting': return 'yellow';
case 'error': return 'orange';
default: return 'gray';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'online': return '✅';
case 'offline': return '❌';
case 'connecting': return '🔄';
case 'error': return '⚠️';
default: return '❓';
}
};
return (
<div className="server-status">
<span className="status-icon">{getStatusIcon(server.status)}</span>
<span className="status-text" style={{ color: getStatusColor(server.status) }}>
{server.status}
</span>
<span className="user-count">{server.users} users</span>
</div>
);
};Troubleshooting
Common Multi-Server Issues
Server connection failures?
# Check server availability
curl -f https://server.example.com/health
# Verify WebSocket connectivity
wscat -c wss://server.example.com/ws
# Check firewall settings
sudo ufw statusRoom isolation problems?
# Verify room ID generation
# Check server name prefixing
# Test with different servers
# Verify channel isolationState synchronization issues?
# Check WebSocket connections
# Verify state persistence
# Test server switching
# Check error logsPerformance issues with multiple servers?
# Monitor connection count
# Check resource usage
# Optimize connection pooling
# Reduce background connectionsDebug Tools
Enable multi-server debugging:
// Enable multi-server debug mode
localStorage.setItem('debug', 'gryt:multiserver:*');
// Multi-server metrics
const multiServerMetrics = {
connectedServers: getConnectedServerCount(),
activeConnections: getActiveConnectionCount(),
roomCount: getTotalRoomCount(),
stateSyncQueue: getStateSyncQueueSize()
};
console.log('Multi-Server Metrics:', multiServerMetrics);Performance Metrics
Multi-Server Performance
Monitor multi-server efficiency:
- Connection Latency: < 100ms per server
- State Sync Time: < 50ms across all servers
- Memory Usage: < 100MB for 10 servers
- CPU Usage: < 10% for connection management
Scalability Metrics
Measure multi-server scalability:
- Max Servers: 50+ concurrent servers
- Max Rooms: 1000+ rooms across all servers
- Max Users: 10,000+ users across all servers
- Connection Pool: 100+ concurrent connections