Spaces:
Running
Running
| import WebSocket from 'ws'; | |
| import { | |
| createRequestId, | |
| safeJsonParse, | |
| sendWsJson | |
| } from '../server/relay-protocol.js'; | |
| import { | |
| buildLocalUrl, | |
| buildLocalWsUrl, | |
| logState | |
| } from './relay-mac-client-config.mjs'; | |
| export function createLocalServiceMonitor({ | |
| getRelaySocket, | |
| getRelayEpoch, | |
| getRelayConnectionId = () => '' | |
| }) { | |
| let localEventWs = null; | |
| let localEventToken = ''; | |
| let localStatus = { reachable: false, checkedAt: '' }; | |
| function sendRelayMessage(payload) { | |
| const socket = getRelaySocket(); | |
| if (!socket || socket.readyState !== socket.OPEN) { | |
| return false; | |
| } | |
| return sendWsJson(socket, { | |
| ...payload, | |
| macConnectionEpoch: getRelayEpoch() | |
| }); | |
| } | |
| function getStatus() { | |
| return localStatus; | |
| } | |
| function closeEventSocket() { | |
| const socket = localEventWs; | |
| localEventWs = null; | |
| localEventToken = ''; | |
| if (socket && socket.readyState === WebSocket.OPEN) { | |
| socket.close(); | |
| } else if (socket && socket.readyState === WebSocket.CONNECTING) { | |
| socket.terminate(); | |
| } | |
| } | |
| function forwardLocalEvent(raw) { | |
| const payload = safeJsonParse(raw.toString()); | |
| if (!payload?.type || payload.type === 'connected') { | |
| return; | |
| } | |
| sendRelayMessage({ | |
| type: 'event', | |
| eventId: payload.eventId || createRequestId(), | |
| payload | |
| }); | |
| } | |
| function ensureEventSocket(token) { | |
| const nextToken = String(token || '').trim(); | |
| if (!nextToken) { | |
| return Promise.resolve(false); | |
| } | |
| if (localEventWs?.readyState === WebSocket.OPEN && localEventToken === nextToken) { | |
| return Promise.resolve(true); | |
| } | |
| closeEventSocket(); | |
| localEventToken = nextToken; | |
| localEventWs = new WebSocket(buildLocalWsUrl(nextToken)); | |
| const socket = localEventWs; | |
| socket.on('message', forwardLocalEvent); | |
| socket.on('close', () => { | |
| if (localEventWs === socket) { | |
| localEventWs = null; | |
| localEventToken = ''; | |
| } | |
| }); | |
| socket.on('error', (error) => { | |
| logState('local-ws-error', error.message); | |
| }); | |
| return new Promise((resolve) => { | |
| socket.once('open', () => resolve(true)); | |
| socket.once('unexpected-response', () => resolve(false)); | |
| socket.once('error', () => resolve(false)); | |
| }); | |
| } | |
| async function checkStatus() { | |
| const checkedAt = new Date().toISOString(); | |
| try { | |
| const response = await fetch(buildLocalUrl('/api/status'), { | |
| headers: { accept: 'application/json' }, | |
| signal: AbortSignal.timeout(10000) | |
| }); | |
| localStatus = { | |
| reachable: response.ok, | |
| status: response.status, | |
| checkedAt | |
| }; | |
| } catch (error) { | |
| localStatus = { | |
| reachable: false, | |
| error: error?.name === 'TimeoutError' ? 'timeout' : 'unreachable', | |
| checkedAt | |
| }; | |
| } | |
| return localStatus; | |
| } | |
| function sendStatus() { | |
| sendRelayMessage({ | |
| type: 'mac.status', | |
| connectionId: getRelayConnectionId(), | |
| localStatus | |
| }); | |
| } | |
| async function startStatusLoop() { | |
| await checkStatus(); | |
| sendStatus(); | |
| setTimeout(startStatusLoop, localStatus.reachable ? 30000 : 5000).unref?.(); | |
| } | |
| return { | |
| checkStatus, | |
| closeEventSocket, | |
| ensureEventSocket, | |
| getStatus, | |
| sendStatus, | |
| startStatusLoop | |
| }; | |
| } | |