Skip to main content
Glama
websocket.ts5.72 kB
import { WebSocketServer, WebSocket } from 'ws'; import net from 'net'; import { getSnapshot, getAvailableActions, sendEvent, type AnyInputRequest, type InputStatus, type TextMachineEvent } from './machine.js'; let wss: WebSocketServer | null = null; const clients = new Set<WebSocket>(); export interface StateUpdate { currentState: string; text: string; contentType: 'text' | 'markdown'; historyCount: number; lastAction: string | null; lastError: string | null; inputRequest: AnyInputRequest | null; inputStatus: InputStatus; userInput: string | null; multiFieldInput?: Record<string, unknown> | null; userContext?: Record<string, unknown>; sidebarVisible: boolean; } // Incoming message types from frontend export interface IncomingMessage { type: 'submit_input' | 'cancel_input' | 'submit_multi_form' | 'toggle_sidebar'; payload?: { requestId?: string; value?: string; values?: Record<string, unknown>; // For multi-field forms }; } export function startWebSocketServer(port: number) { wss = new WebSocketServer({ port }); wss.on('connection', (ws) => { clients.add(ws); console.error(`WebSocket client connected. Total: ${clients.size}`); // Send initial state on connection const snapshot = getSnapshot(); const state = String(snapshot.value); const context = snapshot.context; ws.send( JSON.stringify({ type: 'state_update', data: { currentState: state, text: context.text, contentType: context.contentType, historyCount: context.history.length, lastAction: context.lastAction, lastError: context.lastError, inputRequest: context.inputRequest, inputStatus: context.inputStatus, userInput: context.userInput, userContext: context.userContext, sidebarVisible: context.sidebarVisible, availableActions: getAvailableActions(state, context), }, }) ); // Handle incoming messages from frontend ws.on('message', (data) => { try { const message: IncomingMessage = JSON.parse(data.toString()); handleIncomingMessage(message); } catch (error) { console.error('Failed to parse incoming message:', error); } }); ws.on('close', () => { clients.delete(ws); console.error(`WebSocket client disconnected. Total: ${clients.size}`); }); ws.on('error', (error) => { console.error('WebSocket error:', error); clients.delete(ws); }); }); console.error(`WebSocket server started on port ${port}`); } export function broadcastState(update: StateUpdate) { const snapshot = getSnapshot(); const state = String(snapshot.value); const context = snapshot.context; const message = JSON.stringify({ type: 'state_update', data: { ...update, availableActions: getAvailableActions(state, context), }, timestamp: new Date().toISOString(), }); for (const client of clients) { if (client.readyState === WebSocket.OPEN) { client.send(message); } } } export function getClientCount() { return clients.size; } // Handle incoming messages from frontend function handleIncomingMessage(message: IncomingMessage) { let event: TextMachineEvent; switch (message.type) { case 'submit_input': event = { type: 'SUBMIT_INPUT', value: message.payload?.value || '', requestId: message.payload?.requestId || '', }; break; case 'cancel_input': event = { type: 'CANCEL_INPUT', requestId: message.payload?.requestId || '', }; break; case 'submit_multi_form': event = { type: 'SUBMIT_MULTI_FORM', values: message.payload?.values || {}, requestId: message.payload?.requestId || '', }; break; case 'toggle_sidebar': event = { type: 'TOGGLE_SIDEBAR', }; break; default: console.error('Unknown message type:', message.type); return; } // Send event to state machine const newSnapshot = sendEvent(event); const newState = String(newSnapshot.value); const newContext = newSnapshot.context; // Broadcast updated state to all clients broadcastState({ currentState: newState, text: newContext.text, contentType: newContext.contentType, historyCount: newContext.history.length, lastAction: newContext.lastAction, lastError: newContext.lastError, inputRequest: newContext.inputRequest, inputStatus: newContext.inputStatus, userInput: newContext.userInput, userContext: newContext.userContext, sidebarVisible: newContext.sidebarVisible, }); } export function closeWebSocketServer(): Promise<void> { return new Promise((resolve) => { if (!wss) { resolve(); return; } // Close all client connections for (const client of clients) { client.close(1000, 'Server shutting down'); } clients.clear(); // Close the server wss.close((err) => { if (err) { console.error('Error closing WebSocket server:', err); } else { console.error('WebSocket server closed'); } wss = null; resolve(); }); }); } export function isPortInUse(port: number): Promise<boolean> { return new Promise((resolve) => { const server = net.createServer(); server.once('error', (err: NodeJS.ErrnoException) => { if (err.code === 'EADDRINUSE') { resolve(true); } else { resolve(false); } }); server.once('listening', () => { server.close(); resolve(false); }); server.listen(port); }); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/uptownhr/pane'

If you have feedback or need assistance with the MCP directory API, please join our Discord server