import type { Inspector, InspectorListener } from "."; import { JSC } from ".."; import { WebSocket } from "ws"; export type WebSocketInspectorOptions = { url?: string | URL; listener?: InspectorListener; }; /** * An inspector that communicates with a debugger over a WebSocket. */ export class WebSocketInspector implements Inspector { #url?: URL; #webSocket?: WebSocket; #requestId: number; #pendingRequests: Map void>; #pendingMessages: string[]; #listener: InspectorListener; constructor({ url, listener }: WebSocketInspectorOptions) { this.#url = url ? new URL(url) : undefined; this.#listener = listener ?? {}; this.#requestId = 1; this.#pendingRequests = new Map(); this.#pendingMessages = []; } start(url?: string | URL): void { if (url) { this.#url = new URL(url); } if (this.#url) { this.#connect(); } } #connect(): void { if (!this.#url) { return; } this.#webSocket?.close(); let webSocket: WebSocket; try { console.log("[jsc] connecting", this.#url.href); webSocket = new WebSocket(this.#url, { headers: { "Ref-Event-Loop": "0", }, }); } catch (error) { this.#close(unknownToError(error)); return; } webSocket.addEventListener("open", () => { console.log("[jsc] connected"); for (const message of this.#pendingMessages) { this.#send(message); } this.#pendingMessages.length = 0; this.#listener["Inspector.connected"]?.(); }); webSocket.addEventListener("message", ({ data }) => { if (typeof data === "string") { this.accept(data); } }); webSocket.addEventListener("error", event => { console.log("[jsc] error", event); this.#close(unknownToError(event)); }); webSocket.addEventListener("unexpected-response", () => { console.log("[jsc] unexpected-response"); this.#close(new Error("WebSocket upgrade failed")); }); webSocket.addEventListener("close", ({ code, reason }) => { console.log("[jsc] closed", code, reason); if (code === 1001) { this.#close(); } else { this.#close(new Error(`WebSocket closed: ${code} ${reason}`.trimEnd())); } }); this.#webSocket = webSocket; } send( method: M, params?: JSC.RequestMap[M] | undefined, ): Promise { const id = this.#requestId++; const request = { id, method, params }; console.log("[jsc] -->", request); return new Promise((resolve, reject) => { const done = (result: any) => { this.#pendingRequests.delete(id); if (result instanceof Error) { reject(result); } else { resolve(result); } }; this.#pendingRequests.set(id, done); this.#send(JSON.stringify(request)); }); } #send(message: string): void { if (this.#webSocket) { const { readyState } = this.#webSocket!; if (readyState === WebSocket.OPEN) { this.#webSocket.send(message); } return; } if (!this.#pendingMessages.includes(message)) { this.#pendingMessages.push(message); } } accept(message: string): void { let event: JSC.Event | JSC.Response; try { event = JSON.parse(message); } catch (error) { console.error("Failed to parse message:", message); return; } console.log("[jsc] <--", event); if ("id" in event) { const { id } = event; const resolve = this.#pendingRequests.get(id); if (!resolve) { console.error(`Failed to accept response for unknown ID ${id}:`, event); return; } this.#pendingRequests.delete(id); if ("error" in event) { const { error } = event; const { message } = error; resolve(new Error(message)); } else { const { result } = event; resolve(result); } } else { const { method, params } = event; try { // @ts-ignore this.#listener[method]?.(params); } catch (error) { console.error(`Failed to accept ${method} event:`, error); return; } } } get closed(): boolean { if (!this.#webSocket) { return true; } const { readyState } = this.#webSocket; switch (readyState) { case WebSocket.CLOSED: case WebSocket.CLOSING: return true; } return false; } close(code?: number, reason?: string): void { this.#webSocket?.close(code ?? 1001, reason); } #close(error?: Error): void { try { this.#listener["Inspector.disconnected"]?.(error); } finally { for (const resolve of this.#pendingRequests.values()) { resolve(error ?? new Error("WebSocket closed")); } this.#pendingRequests.clear(); } } } function unknownToError(input: unknown): Error { if (input instanceof Error) { return input; } if (typeof input === "object" && input !== null && "message" in input) { const { message } = input; return new Error(`${message}`); } return new Error(`${input}`); } rove-docker Unnamed repository; edit this file 'description' to name the repository.
aboutsummaryrefslogtreecommitdiff
AgeCommit message (Expand)AuthorFilesLines
2022-04-03wipGravatar Jarred Sumner 120-20/+8988
2022-04-03[bun.js] Expose `ImageData` globallyGravatar Jarred Sumner 12-2/+1237
2022-04-02More typesGravatar Jarred Sumner 2-39/+3998
2022-04-02Add more typingsGravatar Jarred Sumner 1-2/+416
2022-04-02Add more tests for Node FSGravatar Jarred Sumner 4-1/+60
2022-04-02[bun.js] fs.readSync & fs.writeSync should return just the numberGravatar Jarred Sumner 1-24/+57
2022-04-02[bun.js] Support `mode` and `flags` as integer args in fs.openSync (instead o...Gravatar Jarred Sumner 1-0/+6
2022-04-02Update base.zigGravatar Jarred Sumner 1-0/+1
2022-04-02Fix GC bug when reading TypedArray from user inputGravatar Jarred Sumner 1-6/+36
2022-04-02s/Buffer/TypedArrayGravatar Jarred Sumner 1-17/+17
2022-04-02Fix mmap on macOS x64Gravatar Jarred Sumner 2-29/+27