Restructured client
This commit is contained in:
+94
-18
@@ -2,16 +2,28 @@ import { WebSocket } from "ws";
|
|||||||
import { LiveRunMove } from "./types";
|
import { LiveRunMove } from "./types";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
|
/* ──────────────────────────
|
||||||
|
State
|
||||||
|
────────────────────────── */
|
||||||
|
|
||||||
let socket: WebSocket | null = null;
|
let socket: WebSocket | null = null;
|
||||||
|
let connecting: Promise<WebSocket> | null = null;
|
||||||
|
let currentOrigin: string | null = null;
|
||||||
|
|
||||||
let cookies: Record<string, string> | undefined;
|
let cookies: Record<string, string> | undefined;
|
||||||
|
|
||||||
type PendingResolver = {
|
type PendingResolver = {
|
||||||
resolve: (value: any) => void;
|
resolve: (value: any) => void;
|
||||||
reject: (err: any) => void;
|
reject: (err: any) => void;
|
||||||
|
timeout: NodeJS.Timeout;
|
||||||
};
|
};
|
||||||
|
|
||||||
const pending = new Map<string, PendingResolver>();
|
const pending = new Map<string, PendingResolver>();
|
||||||
|
|
||||||
|
/* ──────────────────────────
|
||||||
|
Cookies
|
||||||
|
────────────────────────── */
|
||||||
|
|
||||||
export function getCookies(): Record<string, string> | undefined {
|
export function getCookies(): Record<string, string> | undefined {
|
||||||
return cookies;
|
return cookies;
|
||||||
}
|
}
|
||||||
@@ -20,74 +32,134 @@ export function setCookies(c: Record<string, string>) {
|
|||||||
cookies = c;
|
cookies = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ──────────────────────────
|
||||||
|
Utils
|
||||||
|
────────────────────────── */
|
||||||
|
|
||||||
function getWsOrigin(useLocalhost: boolean) {
|
function getWsOrigin(useLocalhost: boolean) {
|
||||||
return useLocalhost
|
return useLocalhost
|
||||||
? "ws://localhost:3000/api/run"
|
? "ws://localhost:3000/api/run"
|
||||||
: "wss://dev-run.selimaj.dev/api/run";
|
: "wss://dev-run.selimaj.dev/api/run";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rejectAllPending(err: Error) {
|
||||||
|
for (const { reject, timeout } of pending.values()) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
pending.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──────────────────────────
|
||||||
|
Socket management
|
||||||
|
────────────────────────── */
|
||||||
|
|
||||||
async function ensureSocket(useLocalhost: boolean): Promise<WebSocket> {
|
async function ensureSocket(useLocalhost: boolean): Promise<WebSocket> {
|
||||||
|
const origin = getWsOrigin(useLocalhost);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
socket &&
|
socket &&
|
||||||
socket.url === getWsOrigin(useLocalhost) &&
|
socket.readyState === WebSocket.OPEN &&
|
||||||
socket.readyState === WebSocket.OPEN
|
currentOrigin === origin
|
||||||
) {
|
) {
|
||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (socket && socket.url !== getWsOrigin(useLocalhost)) {
|
if (connecting) {
|
||||||
socket.close();
|
return connecting;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
if (socket && currentOrigin !== origin) {
|
||||||
const ws = new WebSocket(getWsOrigin(useLocalhost));
|
socket.close();
|
||||||
|
socket = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
connecting = new Promise<WebSocket>((resolve, reject) => {
|
||||||
|
const ws = new WebSocket(origin);
|
||||||
|
|
||||||
ws.onopen = async () => {
|
ws.onopen = async () => {
|
||||||
|
try {
|
||||||
|
const authCookies = getCookies();
|
||||||
|
if (!authCookies) {
|
||||||
|
throw new Error("Cookies not set");
|
||||||
|
}
|
||||||
|
|
||||||
socket = ws;
|
socket = ws;
|
||||||
|
currentOrigin = origin;
|
||||||
|
connecting = null;
|
||||||
|
|
||||||
console.log("Authenticating with cookies:", getCookies());
|
await sendUnsafe(useLocalhost, {
|
||||||
|
type: "auth",
|
||||||
await sendUnsafe(useLocalhost, { cookies: getCookies() });
|
cookies: authCookies,
|
||||||
|
});
|
||||||
|
|
||||||
resolve(ws);
|
resolve(ws);
|
||||||
};
|
} catch (err) {
|
||||||
|
ws.close();
|
||||||
ws.onerror = (err) => {
|
|
||||||
reject(err);
|
reject(err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
const msg = JSON.parse(e.data.toString());
|
let msg: any;
|
||||||
const { requestId, ok, data, error } = msg;
|
try {
|
||||||
|
msg = JSON.parse(e.data.toString());
|
||||||
|
} catch {
|
||||||
|
console.warn("Invalid WS message:", e.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { requestId, ok, data, error } = msg;
|
||||||
if (!requestId) return;
|
if (!requestId) return;
|
||||||
|
|
||||||
const pendingReq = pending.get(requestId);
|
const pendingReq = pending.get(requestId);
|
||||||
if (!pendingReq) return;
|
if (!pendingReq) return;
|
||||||
|
|
||||||
pending.delete(requestId);
|
pending.delete(requestId);
|
||||||
|
clearTimeout(pendingReq.timeout);
|
||||||
|
|
||||||
ok ? pendingReq.resolve(data) : pendingReq.reject(error);
|
ok ? pendingReq.resolve(data) : pendingReq.reject(error);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ws.onerror = (err) => {
|
||||||
|
rejectAllPending(new Error("WebSocket error"));
|
||||||
|
connecting = null;
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
|
rejectAllPending(new Error("WebSocket closed"));
|
||||||
socket = null;
|
socket = null;
|
||||||
|
currentOrigin = null;
|
||||||
|
connecting = null;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return connecting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ──────────────────────────
|
||||||
|
RPC helpers
|
||||||
|
────────────────────────── */
|
||||||
|
|
||||||
async function sendUnsafe<T>(
|
async function sendUnsafe<T>(
|
||||||
useLocalhost: boolean,
|
useLocalhost: boolean,
|
||||||
payload: Record<string, any>,
|
payload: Record<string, any>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
if (!socket) {
|
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||||
throw new Error("WebSocket is not connected");
|
throw new Error("WebSocket is not open");
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestId = randomUUID();
|
const requestId = randomUUID();
|
||||||
|
|
||||||
return new Promise<T>((resolve, reject) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
pending.set(requestId, { resolve, reject });
|
const timeout = setTimeout(() => {
|
||||||
|
pending.delete(requestId);
|
||||||
|
reject(new Error("WebSocket request timed out"));
|
||||||
|
}, 10_000);
|
||||||
|
|
||||||
|
pending.set(requestId, { resolve, reject, timeout });
|
||||||
|
|
||||||
socket?.send(JSON.stringify({ ...payload, requestId }));
|
socket?.send(JSON.stringify({ ...payload, requestId }));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -97,9 +169,13 @@ async function send<T>(
|
|||||||
payload: Record<string, any>,
|
payload: Record<string, any>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
await ensureSocket(useLocalhost);
|
await ensureSocket(useLocalhost);
|
||||||
return await sendUnsafe(useLocalhost, payload);
|
return sendUnsafe(useLocalhost, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ──────────────────────────
|
||||||
|
Public API
|
||||||
|
────────────────────────── */
|
||||||
|
|
||||||
export async function addRun(
|
export async function addRun(
|
||||||
useLocalhost: boolean,
|
useLocalhost: boolean,
|
||||||
problem: string,
|
problem: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user