Files
huskies/frontend/src/api/rpc.ts
T

108 lines
2.5 KiB
TypeScript
Raw Normal View History

2026-04-28 15:31:29 +00:00
/**
* Lightweight read-RPC client over the `/ws` WebSocket.
*
* Opens a short-lived WebSocket, sends an `rpc_request` frame, waits for the
* matching `rpc_response`, then closes the connection.
*/
let correlationCounter = 0;
function nextCorrelationId(): string {
return `rpc-${Date.now()}-${++correlationCounter}`;
}
/**
* Build the WebSocket URL for the `/ws` endpoint, deriving the protocol
* (ws/wss) and host from the current page location.
*/
function buildWsUrl(): string {
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
return `${proto}//${window.location.host}/ws`;
}
export interface RpcResponse<T = unknown> {
ok: boolean;
result?: T;
error?: string;
code?: string;
}
/**
* Send a read-RPC request over a temporary WebSocket connection and return
* the result. Rejects if the server responds with `ok: false` or if the
* connection times out.
*/
export function rpcCall<T = unknown>(
method: string,
params: Record<string, unknown> = {},
timeoutMs = 5000,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
const correlationId = nextCorrelationId();
const ws = new WebSocket(buildWsUrl());
let settled = false;
const timer = setTimeout(() => {
if (!settled) {
settled = true;
ws.close();
reject(new Error(`RPC timeout for ${method}`));
}
}, timeoutMs);
ws.onopen = () => {
ws.send(
JSON.stringify({
kind: "rpc_request",
version: 1,
correlation_id: correlationId,
ttl_ms: timeoutMs,
method,
params,
}),
);
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// Only process rpc_response frames matching our correlation ID.
if (
data.kind === "rpc_response" &&
data.correlation_id === correlationId
) {
settled = true;
clearTimeout(timer);
ws.close();
if (data.ok) {
resolve(data.result as T);
} else {
reject(
new Error(data.error || `RPC error: ${data.code || "UNKNOWN"}`),
);
}
}
// Ignore other messages (pipeline_state, onboarding_status, etc.)
} catch {
// Ignore non-JSON or unparseable messages
}
};
ws.onerror = () => {
if (!settled) {
settled = true;
clearTimeout(timer);
reject(new Error(`WebSocket error during RPC call to ${method}`));
}
};
ws.onclose = () => {
if (!settled) {
settled = true;
clearTimeout(timer);
reject(new Error(`WebSocket closed before RPC response for ${method}`));
}
};
});
}