codexmobile-relay / scripts /relay-mac-client-local.mjs
Codex
deploy: CodexMobile Relay
90f0300
Raw
History Blame Contribute Delete
3.32 kB
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
};
}