* LSP / tsserver 共用的 Content-Length 分帧。
* 只在 stdio 上跑,不做网络相关的兼容;对 \n\n 分隔也容忍。
*/
const MAX_HEADER_BYTES = 8 * 1024;
export const MAX_FRAME_BYTES = 16 * 1024 * 1024;
export function encodeFrame(message: unknown): Buffer {
const json = Buffer.from(JSON.stringify(message), "utf8");
return Buffer.concat([
Buffer.from(`Content-Length: ${json.length}\r\n\r\n`, "ascii"),
json,
]);
}
export class FrameParser {
private buffer: Buffer = Buffer.alloc(0);
constructor(
private readonly onMessage: (message: unknown) => void,
private readonly onError?: (error: Error) => void,
) {}
feed(chunk: Buffer): void {
this.buffer = this.buffer.length ? Buffer.concat([this.buffer, chunk]) : chunk;
for (;;) {
const crlf = this.buffer.indexOf("\r\n\r\n");
const lf = this.buffer.indexOf("\n\n");
let headerEnd: number;
let separatorLength: number;
if (crlf >= 0 && (lf < 0 || crlf <= lf)) {
headerEnd = crlf;
separatorLength = 4;
} else if (lf >= 0) {
headerEnd = lf;
separatorLength = 2;
} else {
if (this.buffer.length > MAX_HEADER_BYTES) {
this.onError?.(new Error("LSP message header is too large."));
this.buffer = Buffer.alloc(0);
}
return;
}
if (headerEnd > MAX_HEADER_BYTES) {
this.onError?.(new Error("LSP message header is too large."));
this.buffer = Buffer.alloc(0);
return;
}
const header = this.buffer.subarray(0, headerEnd).toString("ascii");
const match = /(?:^|\r?\n)Content-Length:\s*(\d+)/i.exec(header);
if (!match) {
this.onError?.(new Error("LSP message is missing Content-Length."));
this.buffer = this.buffer.subarray(headerEnd + separatorLength);
continue;
}
const length = Number(match[1]);
if (!Number.isInteger(length) || length < 0 || length > MAX_FRAME_BYTES) {
this.onError?.(new Error(`Invalid LSP Content-Length: ${match[1]}`));
this.buffer = Buffer.alloc(0);
return;
}
const bodyStart = headerEnd + separatorLength;
if (this.buffer.length < bodyStart + length) return;
const body = this.buffer.subarray(bodyStart, bodyStart + length).toString("utf8");
this.buffer = this.buffer.subarray(bodyStart + length);
let parsed: unknown;
try {
parsed = JSON.parse(body);
} catch (error) {
this.onError?.(error instanceof Error ? error : new Error(String(error)));
continue;
}
this.onMessage(parsed);
}
}
}