diff --git a/package-lock.json b/package-lock.json index 126fedc..6a475a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,12 @@ "@types/cors": "^2.8.19", "@types/diff-match-patch": "^1.0.36", "@types/express": "^5.0.6", + "@types/ws": "^8.18.1", "axios": "^1.13.5", "cors": "^2.8.6", "diff-match-patch": "^1.0.5", - "express": "^5.2.1" + "express": "^5.2.1", + "ws": "^8.19.0" }, "devDependencies": { "@types/mocha": "^10.0.6", @@ -407,6 +409,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", @@ -4140,6 +4151,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 14f640e..3a4bd61 100644 --- a/package.json +++ b/package.json @@ -91,9 +91,11 @@ "@types/cors": "^2.8.19", "@types/diff-match-patch": "^1.0.36", "@types/express": "^5.0.6", + "@types/ws": "^8.18.1", "axios": "^1.13.5", "cors": "^2.8.6", "diff-match-patch": "^1.0.5", - "express": "^5.2.1" + "express": "^5.2.1", + "ws": "^8.19.0" } } diff --git a/src/client.ts b/src/client.ts index a2116bb..f7803ba 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,16 +1,78 @@ -import axios from "axios"; +import { WebSocket } from "ws"; import { LiveRunMove } from "./types"; +import { randomUUID } from "crypto"; -var cookies: string | undefined; +let socket: WebSocket | null = null; +let cookies: string | undefined; + +type PendingResolver = { + resolve: (value: any) => void; + reject: (err: any) => void; +}; + +const pending = new Map(); export function getCookies(): string | undefined { return cookies; } + export function setCookies(c: string) { cookies = c; } -function getOrigin(useLocalhost: boolean) { - return useLocalhost ? "http://localhost:3000" : "https://dev-run.selimaj.dev"; + +function getWsOrigin(useLocalhost: boolean) { + return useLocalhost + ? "ws://localhost:3000/api/run" + : "wss://dev-run.selimaj.dev/api/run"; +} + +function ensureSocket(useLocalhost: boolean): WebSocket { + if (socket && socket.readyState === WebSocket.OPEN) { + return socket; + } + + socket = new WebSocket(getWsOrigin(useLocalhost)); + + socket.onmessage = (e) => { + const msg = JSON.parse(e.data.toString()); + const { requestId, ok, data, error } = msg; + + if (!requestId) return; + + const pendingReq = pending.get(requestId); + if (!pendingReq) return; + + pending.delete(requestId); + + ok ? pendingReq.resolve(data) : pendingReq.reject(error); + }; + + socket.onclose = () => { + socket = null; + }; + + return socket; +} + +function send( + useLocalhost: boolean, + payload: Record, +): Promise { + return new Promise((resolve, reject) => { + const ws = ensureSocket(useLocalhost); + + const requestId = randomUUID(); + + pending.set(requestId, { resolve, reject }); + + ws.send( + JSON.stringify({ + ...payload, + requestId, + cookies, // optional: only needed if you authenticate via cookies manually + }), + ); + }); } export async function addRun( @@ -18,34 +80,20 @@ export async function addRun( problem: string, mode: string, ): Promise { - return ( - await axios.put( - getOrigin(useLocalhost) + "/api/run", - { - problem, - category: mode, - }, - { - headers: { - Cookie: cookies, - }, - }, - ) - ).data.runId; + const data = await send<{ runId: string }>(useLocalhost, { + type: "create", + problem, + category: mode, + }); + + return data.runId; } export async function submitRun(useLocalhost: boolean, runId: string) { - await axios.post( - getOrigin(useLocalhost) + "/api/run", - { - runId, - }, - { - headers: { - Cookie: cookies, - }, - }, - ); + await send(useLocalhost, { + type: "submit", + runId, + }); } export async function addRunMoves( @@ -55,18 +103,11 @@ export async function addRunMoves( language: string | null, moves: LiveRunMove[], ) { - await axios.post( - getOrigin(useLocalhost) + "/api/run/move", - { - runId, - file, - moves, - language, - }, - { - headers: { - Cookie: cookies, - }, - }, - ); + await send(useLocalhost, { + type: "move", + runId, + file, + language, + moves, + }); }