huskies: merge 770
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 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}`));
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user