ttsc
Version:
General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.
213 lines • 7.41 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ResidentCheckProcess = void 0;
const node_child_process_1 = require("node:child_process");
const node_readline_1 = require("node:readline");
const STDERR_TAIL_LIMIT = 64 * 1024;
const REPLY_ECHO_LIMIT = 200;
const TERMINATION_GRACE_MS = 1_000;
/**
* FIFO JSON-line client for a check-stage `check-serve` sidecar.
*
* A watch session serializes cycles, but the client still queues replies so a
* caller cannot accidentally pair a late response with the next request. Any
* framing failure retires the process; the launcher then falls back to the
* ordinary one-shot command for that cycle.
*/
class ResidentCheckProcess {
child;
pending = [];
reader;
failure;
stderr = "";
constructor(options) {
this.child = (0, node_child_process_1.spawn)(options.binary, [...options.args], {
cwd: options.cwd,
env: options.env,
windowsHide: true,
});
const stdin = this.child.stdin;
const stdout = this.child.stdout;
if (stdin === null || stdout === null) {
this.child.kill();
throw new Error("ttsc: resident check host has no stdio pipes");
}
this.reader = (0, node_readline_1.createInterface)({ input: stdout });
this.reader.on("line", (line) => this.onLine(line));
this.reader.on("close", () => {
if (this.failure === undefined)
this.fail(this.exitError());
});
this.child.stderr?.on("data", (chunk) => {
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
this.stderr = (this.stderr + text).slice(-STDERR_TAIL_LIMIT);
});
this.child.on("error", (error) => this.fail(error));
stdin.on("error", (error) => this.fail(error));
stdout.on("error", (error) => this.fail(error));
this.child.stderr?.on("error", () => { });
}
request(payload) {
if (this.failure !== undefined)
return Promise.reject(this.failure);
const stdin = this.child.stdin;
if (stdin === null || stdin.destroyed) {
return Promise.reject(new Error("ttsc: resident check host stdin is closed"));
}
let line;
try {
line = `${JSON.stringify(payload)}\n`;
}
catch (error) {
return Promise.reject(asError(error));
}
return new Promise((resolve, reject) => {
const pending = { reject, resolve };
this.pending.push(pending);
try {
stdin.write(line, (error) => {
if (error === null || error === undefined)
return;
this.settle(pending, error);
this.fail(error);
});
}
catch (error) {
const reason = asError(error);
this.settle(pending, reason);
this.fail(reason);
}
});
}
dispose() {
if (this.failure !== undefined)
return;
this.fail(new Error("ttsc: resident check host disposed"));
}
onLine(line) {
if (this.failure !== undefined || line.trim().length === 0)
return;
const pending = this.pending[0];
if (pending === undefined) {
this.fail(new Error("ttsc: resident check host sent an unsolicited reply"));
return;
}
const result = parseResidentCheckResult(line);
if (result === undefined) {
const error = new Error(`ttsc: resident check host sent a malformed reply: ${echoLine(line)}`);
this.settle(pending, error);
this.fail(error);
return;
}
this.settle(pending, result);
}
settle(pending, result) {
const index = this.pending.indexOf(pending);
if (index === -1)
return;
this.pending.splice(index, 1);
if (result instanceof Error)
pending.reject(result);
else
pending.resolve(result);
}
fail(error) {
if (this.failure !== undefined)
return;
this.failure = error;
while (this.pending.length !== 0) {
this.settle(this.pending[0], error);
}
this.terminate();
}
terminate() {
const stdin = this.child.stdin;
if (stdin !== null && !stdin.destroyed)
stdin.destroy();
if (this.child.exitCode !== null || this.child.signalCode !== null)
return;
try {
this.child.kill();
}
catch {
return;
}
const force = setTimeout(() => {
if (this.child.exitCode !== null || this.child.signalCode !== null)
return;
try {
this.child.kill("SIGKILL");
}
catch {
// The host exited between the liveness check and forced termination.
}
}, TERMINATION_GRACE_MS);
force.unref();
this.child.once("exit", () => clearTimeout(force));
}
exitError() {
const detail = this.stderr.trim();
if (detail.length !== 0)
return new Error(detail);
const signal = this.child.signalCode === null ? "" : `, signal ${this.child.signalCode}`;
return new Error(`ttsc: resident check host exited (code ${this.child.exitCode ?? "null"}${signal})`);
}
}
exports.ResidentCheckProcess = ResidentCheckProcess;
function parseResidentCheckResult(line) {
let value;
try {
value = JSON.parse(line);
}
catch {
return undefined;
}
if (!isRecord(value) ||
typeof value.status !== "number" ||
!Number.isInteger(value.status)) {
return undefined;
}
if (typeof value.stdout !== "string" || typeof value.stderr !== "string") {
return undefined;
}
const telemetry = value.telemetry;
if (!isRecord(telemetry) ||
typeof telemetry.pid !== "number" ||
!Number.isSafeInteger(telemetry.pid) ||
typeof telemetry.programLoads !== "number" ||
!Number.isSafeInteger(telemetry.programLoads) ||
typeof telemetry.programUpdates !== "number" ||
!Number.isSafeInteger(telemetry.programUpdates) ||
typeof telemetry.reused !== "boolean") {
return undefined;
}
return {
diagnostics: [],
status: value.status,
stderr: value.stderr,
stdout: value.stdout,
telemetry: {
pid: telemetry.pid,
programLoads: telemetry.programLoads,
programUpdates: telemetry.programUpdates,
reused: telemetry.reused,
},
};
}
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function echoLine(line) {
const trimmed = line.trim();
return JSON.stringify(trimmed.length <= REPLY_ECHO_LIMIT
? trimmed
: `${trimmed.slice(0, REPLY_ECHO_LIMIT)}…`);
}
function stderrSuffix(stderr) {
const detail = stderr.trim();
return detail.length === 0 ? "" : `: ${detail}`;
}
function asError(error) {
return error instanceof Error ? error : new Error(String(error));
}
//# sourceMappingURL=residentCheckProcess.js.map