UNPKG

ble-mcp-test

Version:

Complete BLE testing stack: WebSocket bridge server, MCP observability layer, and Web Bluetooth API mock. Test real BLE devices in Playwright/E2E tests without browser support.

145 lines (144 loc) 5.05 kB
export class WebSocketTransport { ws = null; serverUrl; messageHandler; connectionToken; // v0.4.0: Store token for force cleanup sessionId; // v0.4.5: Session management constructor(serverUrl = 'ws://localhost:8080') { this.serverUrl = serverUrl; } async connect(options) { const url = new URL(this.serverUrl); if (options?.device) url.searchParams.set('device', options.device); if (options?.service) url.searchParams.set('service', options.service); if (options?.write) url.searchParams.set('write', options.write); if (options?.notify) url.searchParams.set('notify', options.notify); // Session management if (options?.session) { url.searchParams.set('session', options.session); this.sessionId = options.session; } this.ws = new WebSocket(url.toString()); return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Connection timeout')); }, 10000); this.ws.onopen = () => { // WebSocket opened, wait for connected message }; this.ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.type === 'connected') { clearTimeout(timeout); // v0.4.0: Store token for force cleanup if (msg.token) { this.connectionToken = msg.token; } resolve(); } else if (msg.type === 'error') { clearTimeout(timeout); reject(new Error(msg.error || 'Connection failed')); } } catch { // Ignore invalid messages } }; this.ws.onerror = () => { clearTimeout(timeout); reject(new Error('WebSocket error')); }; this.ws.onclose = () => { this.ws = null; if (this.messageHandler) { this.messageHandler({ type: 'disconnected' }); } }; }); } send(data) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { throw new Error('Not connected'); } this.ws.send(JSON.stringify({ type: 'data', data: Array.from(data) })); } onMessage(callback) { this.messageHandler = callback; if (this.ws) { this.ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (this.messageHandler) { this.messageHandler(msg); } } catch { // Ignore invalid messages } }; } } async forceCleanup() { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { throw new Error('Not connected'); } return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Force cleanup timeout')); }, 5000); // Store reference to WebSocket const ws = this.ws; const originalHandler = ws.onmessage; // Listen for cleanup confirmation ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.type === 'cleanup_complete' || msg.type === 'force_cleanup_complete') { clearTimeout(timeout); ws.onmessage = originalHandler; resolve(); } else if (originalHandler) { originalHandler.call(ws, event); } } catch { if (originalHandler) originalHandler.call(ws, event); } }; // Send force cleanup request // v0.4.0: Include token for authentication const request = { type: 'force_cleanup' }; if (this.connectionToken) { request.token = this.connectionToken; } ws.send(JSON.stringify(request)); }); } disconnect() { if (this.ws) { this.ws.close(); this.ws = null; } } isConnected() { return this.ws !== null && this.ws.readyState === WebSocket.OPEN; } // Session management methods getSessionId() { return this.sessionId; } async reconnectToSession(sessionId) { return this.connect({ session: sessionId }); } }