// A DevTools client for JavaScriptCore. import type { JSC } from ".."; type ClientOptions = { url: string | URL; event?: (event: JSC.Event) => void; request?: (request: JSC.Request) => void; response?: (response: JSC.Response) => void; }; class Client { #webSocket: WebSocket; #requestId: number; #pendingMessages: string[]; #pendingRequests: Map; #ready: Promise; constructor(options: ClientOptions) { this.#webSocket = new WebSocket(options.url); this.#requestId = 1; this.#pendingMessages = []; this.#pendingRequests = new Map(); this.#ready = new Promise((resolve, reject) => { this.#webSocket.addEventListener("open", () => { for (const message of this.#pendingMessages) { this.#send(message); } this.#pendingMessages.length = 0; resolve(); }); this.#webSocket.addEventListener("message", ({ data }) => { let response; try { response = { ...JSON.parse(data) }; } catch { console.error("Received an invalid message:", data); return; } const { id, error, result, method, params } = response; if (method && params) { options.event?.(response); } else if (id && (result || error)) { try { options.response?.(response); } finally { const abort = this.#pendingRequests.get(id ?? -1); if (!abort) { console.error("Received an unexpected message:", response); return; } if (error) { abort.abort(new Error(JSON.stringify(error))); } else { abort.abort(result); } } } else { console.error("Received an unexpected message:", response); } }); this.#webSocket.addEventListener("error", (error) => { reject(error); }); this.#webSocket.addEventListener("close", ({ code, reason = ""}) => { reject(new Error(`WebSocket closed: ${code} ${reason}`.trimEnd())); }); }); } get ready(): Promise { return this.#ready; } #send(message: string): void { const { readyState } = this.#webSocket; if (readyState === WebSocket.OPEN) { this.#webSocket.send(message); } else if (readyState === WebSocket.CONNECTING) { this.#pendingMessages.push(message); } else { const closed = readyState === WebSocket.CLOSING ? "closing" : "closed"; throw new Error(`WebSocket is ${closed}`); } } async fetch(method: T, params: JSC.Request["params"]): Promise> { const request: JSC.Request = { id: this.#requestId++, method, params, }; return new Promise((resolve, reject) => { const abort = new AbortController(); abort.signal.addEventListener("abort", () => { this.#pendingRequests.delete(request.id); const { reason } = abort.signal; if (reason instanceof Error) { reject(reason); } else { resolve(reason); } }); this.#pendingRequests.set(request.id, abort); this.#send(JSON.stringify(request)); }); } } const client = new Client({ url: "ws://localhost:9229", event: (event) => console.log("EVENT:", event), request: (request) => console.log("REQUEST:", request), response: (response) => console.log("RESPONSE:", response), }); await client.ready; while (true) { const [method, ...param] = prompt(">")?.split(" ") ?? []; if (!method.trim()) { continue; } const params = !param?.length ? {} : JSON.parse(eval(`JSON.stringify(${param.join(" ")})`)); try { await client.fetch(method.trim() as any, params); } catch (error) { console.error(error); } }