UNPKG

typescript

Version:

TypeScript is a language for application scale JavaScript development

211 lines • 8.51 kB
import { createMessageConnection, RequestType, SocketMessageReader, SocketMessageWriter, StreamMessageReader, StreamMessageWriter, } from "#vscode-jsonrpc/node"; import { fsCallbackNames, } from "../fs.js"; import { isSpawnOptions, resolveExePath, } from "../options.js"; import { combineTimingInfo, disabledServerTimingInfo, disabledTimingInfo, TimingCollector, } from "../timing.js"; /** * Client handles communication with the TypeScript API server * over STDIO (spawned process) or a Unix domain socket using JSON-RPC. */ export class Client { socket; process; connection; options; connected = false; timing; constructor(options) { this.options = options; if (isSpawnOptions(options) && options.collectTiming) { this.timing = new TimingCollector(); } } async connect() { if (this.connected) return; if (isSpawnOptions(this.options)) { await this.connectViaSpawn(this.options); } else { await this.connectViaSocket(this.options); } } async connectViaSpawn(options) { const { spawn } = await import("node:child_process"); return new Promise((resolve, reject) => { const args = [ "--api", "--async", "--cwd", options.cwd ?? process.cwd(), ]; if (options.collectTiming) { args.push("--timing"); } // Enable virtual FS callbacks for each provided FS function const enabledCallbacks = []; if (options.fs) { for (const name of fsCallbackNames) { if (options.fs[name]) { enabledCallbacks.push(name); } } } if (enabledCallbacks.length > 0) { args.push(`--callbacks=${enabledCallbacks.join(",")}`); } this.process = spawn(resolveExePath(options), args, { stdio: ["pipe", "pipe", "inherit"], }); this.process.once("error", error => { reject(new Error(`Failed to start tsgo process: ${error.message}`)); }); this.process.once("spawn", () => { this.connected = true; resolve(); }); const reader = new StreamMessageReader(this.process.stdout); const writer = new StreamMessageWriter(this.process.stdin); this.connection = createMessageConnection(reader, writer); this.registerFSCallbacks(this.connection, options.fs); this.connection.listen(); }); } async connectViaSocket(options) { const { createConnection } = await import("node:net"); return new Promise((resolve, reject) => { this.socket = createConnection(options.pipe, () => { const reader = new SocketMessageReader(this.socket); const writer = new SocketMessageWriter(this.socket); this.connection = createMessageConnection(reader, writer); this.connection.listen(); this.connected = true; resolve(); }); this.socket.once("error", error => { reject(new Error(`Socket error: ${error.message}`)); }); }); } registerFSCallbacks(connection, fs) { if (!fs) return; for (const name of fsCallbackNames) { const callback = fs[name]; if (callback) { const requestType = new RequestType(name); connection.onRequest(requestType, (arg) => { const result = callback(arg); if (name === "readFile") { // readFile has 3 returns: string (content), null (not found), undefined (fall back). // JSON-RPC can't distinguish null from undefined, so wrap in object. if (result === undefined) return null; return { content: result }; } return result ?? null; }); } } } async apiRequest(method, params) { if (!this.connected) { await this.connect(); } if (!this.connection) { throw new Error("Connection not established"); } const requestType = new RequestType(method); if (!this.timing) { return this.connection.sendRequest(requestType, params); } // Round-trip latency is measured here; byte counts approximate the wire // payload via the serialized JSON. Server-side processing time is not // carried on the response; it is retrieved separately (via a // getServerTiming request) and folded in by getTimingInfo(). const bytesSent = params === undefined ? 0 : Buffer.byteLength(JSON.stringify(params), "utf-8"); const start = performance.now(); const result = await this.connection.sendRequest(requestType, params); const roundTripMs = performance.now() - start; this.timing.record({ method, roundTripMs, bytesSent, bytesReceived: result === undefined || result === null ? 0 : Buffer.byteLength(JSON.stringify(result), "utf-8"), }); return result; } async apiRequestBinary(method, params) { const response = await this.apiRequest(method, params); if (!response) return undefined; const buffer = Buffer.from(response.data, "base64"); return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); } /** * Returns the timing collector that per-node materialization is reported * into, or undefined when timing collection is disabled. The returned * collector is the same one folded into {@link getTimingInfo}, so * materialization totals surface alongside request timings. */ getTimingCollector() { return this.timing; } /** * Returns a combined timing snapshot: client-measured round-trip and byte * counts folded together with the server's own per-request processing time * (fetched via a getServerTiming request) and estimated transport overhead. */ async getTimingInfo() { if (!this.timing) { return disabledTimingInfo(); } const local = this.timing.getInfo(); // No requests have been sent yet: nothing to fetch from the server. if (!this.connected || !this.connection) { return local; } return combineTimingInfo(local, await this.fetchServerTiming()); } async resetTimingInfo() { if (!this.timing) return; this.timing.reset(); if (this.connected && this.connection) { // Keep the server's collection in sync so combined totals stay meaningful. const requestType = new RequestType("resetServerTiming"); await this.connection.sendRequest(requestType, undefined); } } async fetchServerTiming() { if (!this.connection) { return disabledServerTimingInfo(); } // Fetch the server's own timing collection via a dedicated request. This // bypasses the client-side collector so the query does not pollute it. const requestType = new RequestType("getServerTiming"); return this.connection.sendRequest(requestType, undefined); } async close() { if (this.connection) { this.connection.dispose(); this.connection = undefined; } if (this.socket) { this.socket.destroy(); this.socket = undefined; } if (this.process) { // Close stdin to unblock the server's read loop, allowing it to exit cleanly. // The server is blocked on stdin.Read(), so just sending SIGTERM would deadlock: // - Node won't exit while child is alive // - Child can't process SIGTERM while blocked on read // - Read won't error until stdin is closed this.process.stdin?.end(); this.process = undefined; } this.connected = false; } } //# sourceMappingURL=client.js.map