refactor(core): migrate to Bun and converge acquisition policies and Agent boundaries
This commit is contained in:
1 parent
3f1c6d02f6
commit
27c0f27434
268 files changed
+7468
-7852
No files matched your search
@@ -0,0 +1,262 @@
|
||||
import { Readable } from "node:stream";
|
||||
import { type ChildProcess, Command, runtimeWriteSync } from "./index.ts";
|
||||
|
||||
/** 本地执行协议。业务类型、重试、权限与超时策略由调用方负责。 */
|
||||
export interface WorkerRequest {
|
||||
id: string;
|
||||
task: string;
|
||||
input: unknown;
|
||||
}
|
||||
export interface WorkerError {
|
||||
code: string;
|
||||
message: string;
|
||||
detail?: Record<string, unknown>;
|
||||
}
|
||||
export type WorkerReply =
|
||||
| { id: string; type: "result"; result: unknown }
|
||||
| { id: string; type: "error"; error: WorkerError };
|
||||
export type WorkerMessage = WorkerReply | { id: string; type: "progress"; value: unknown };
|
||||
|
||||
export class WorkerTransportError extends Error {
|
||||
constructor(
|
||||
readonly kind: "spawn" | "protocol" | "exit" | "size",
|
||||
message: string,
|
||||
override readonly cause?: unknown,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "WorkerTransportError";
|
||||
}
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
export function encodeWorkerMessage(value: WorkerRequest | WorkerMessage, maxBytes: number): Uint8Array {
|
||||
const bytes = encoder.encode(`${JSON.stringify(value)}\n`);
|
||||
if (bytes.byteLength > maxBytes) throw new WorkerTransportError("size", `worker 消息超过 ${maxBytes} 字节`);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/** 按字节限制一行的大小;UTF-8 与换行可以跨管道分块。EOF 不能代替消息结束符。 */
|
||||
export async function* workerLines(
|
||||
reader: ReadableStreamDefaultReader<Uint8Array>,
|
||||
maxBytes: number,
|
||||
): AsyncGenerator<string> {
|
||||
const decoder = new TextDecoder("utf-8", { fatal: true });
|
||||
let parts: string[] = [];
|
||||
let length = 0;
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
if (length) throw new WorkerTransportError("protocol", "worker 消息未以换行结束");
|
||||
return;
|
||||
}
|
||||
let start = 0;
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
if (value[i] !== 10) continue;
|
||||
length += i - start + 1;
|
||||
if (length > maxBytes) throw new WorkerTransportError("size", `worker 消息超过 ${maxBytes} 字节`);
|
||||
parts.push(decoder.decode(value.subarray(start, i), { stream: true }), decoder.decode());
|
||||
const line = parts.join("");
|
||||
parts = [];
|
||||
length = 0;
|
||||
start = i + 1;
|
||||
if (!line) throw new WorkerTransportError("protocol", "worker 返回空消息");
|
||||
yield line;
|
||||
}
|
||||
length += value.length - start;
|
||||
if (length > maxBytes) throw new WorkerTransportError("size", `worker 消息超过 ${maxBytes} 字节`);
|
||||
parts.push(decoder.decode(value.subarray(start), { stream: true }));
|
||||
}
|
||||
}
|
||||
|
||||
export async function readWorkerRequestText(maxBytes: number): Promise<string> {
|
||||
const stream = Readable.toWeb(process.stdin) as unknown as ReadableStream<Uint8Array>;
|
||||
const reader = stream.getReader();
|
||||
try {
|
||||
let text: string | undefined;
|
||||
for await (const line of workerLines(reader, maxBytes)) {
|
||||
if (text !== undefined) throw new WorkerTransportError("protocol", "一次性 worker 只能接收一个请求");
|
||||
text = line;
|
||||
}
|
||||
if (text === undefined) throw new WorkerTransportError("protocol", "worker 没有收到请求");
|
||||
return text;
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeWorkerRequest(text: string): WorkerRequest {
|
||||
const req = JSON.parse(text) as WorkerRequest;
|
||||
if (!req || typeof req.id !== "string" || !req.id || typeof req.task !== "string" || !("input" in req))
|
||||
throw new WorkerTransportError("protocol", "worker 请求格式无效");
|
||||
return req;
|
||||
}
|
||||
|
||||
export function writeWorkerMessage(message: WorkerMessage, maxBytes: number): void {
|
||||
const bytes = encodeWorkerMessage(message, maxBytes);
|
||||
let offset = 0;
|
||||
while (offset < bytes.length) offset += runtimeWriteSync(bytes.subarray(offset));
|
||||
}
|
||||
|
||||
function decodeMessage(line: string, id: string): WorkerMessage {
|
||||
const value = JSON.parse(line) as WorkerMessage;
|
||||
if (!value || value.id !== id) throw new WorkerTransportError("protocol", "worker 响应不属于当前请求");
|
||||
if (value.type === "result" && "result" in value) return value;
|
||||
if (value.type === "progress" && "value" in value) return value;
|
||||
if (value.type === "error" && typeof value.error?.code === "string" && typeof value.error?.message === "string")
|
||||
return value;
|
||||
throw new WorkerTransportError("protocol", "worker 响应格式无效");
|
||||
}
|
||||
|
||||
/** 一个一次性进程连接。只管通信与回收,不重试、不决定超时、不安装权限策略。 */
|
||||
export class WorkerConnection {
|
||||
readonly pid: number;
|
||||
private readonly child: ChildProcess;
|
||||
private readonly stdout: ReadableStreamDefaultReader<Uint8Array>;
|
||||
private readonly stderrReader: ReadableStreamDefaultReader<Uint8Array>;
|
||||
private reject: ((error: unknown) => void) | null = null;
|
||||
private terminating: unknown;
|
||||
private killTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
private settled = false;
|
||||
private started = false;
|
||||
private stderr = "";
|
||||
|
||||
constructor(command: { cmd: string; args: string[] }, env?: { values: Record<string, string>; clear?: boolean }) {
|
||||
try {
|
||||
this.child = new Command(command.cmd, {
|
||||
args: command.args,
|
||||
stdin: "piped",
|
||||
stdout: "piped",
|
||||
stderr: "piped",
|
||||
env: env?.values,
|
||||
clearEnv: env?.clear,
|
||||
}).spawn();
|
||||
} catch (error) {
|
||||
throw new WorkerTransportError("spawn", "worker 子进程无法启动", error);
|
||||
}
|
||||
this.pid = this.child.pid;
|
||||
this.stdout = this.child.stdout.getReader();
|
||||
this.stderrReader = this.child.stderr.getReader();
|
||||
}
|
||||
|
||||
/** graceMs > 0 时先 SIGTERM 供文件操作清理;解析可直接 SIGKILL。等待始终有界。 */
|
||||
terminate(error: unknown, graceMs = 0, onForceKill?: () => void): void {
|
||||
if (this.settled || this.terminating !== undefined) return;
|
||||
this.terminating = error;
|
||||
const force = () => {
|
||||
try {
|
||||
this.child.kill("SIGKILL");
|
||||
onForceKill?.();
|
||||
} catch {
|
||||
// 已退出
|
||||
}
|
||||
this.child.unref();
|
||||
this.fail(error);
|
||||
};
|
||||
if (graceMs === 0) {
|
||||
force();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.child.kill("SIGTERM");
|
||||
} catch {
|
||||
// 已退出
|
||||
}
|
||||
this.killTimer = setTimeout(force, graceMs);
|
||||
}
|
||||
|
||||
request(
|
||||
req: WorkerRequest,
|
||||
options: { maxRequestBytes: number; maxResponseBytes: number; onProgress?: (value: unknown) => void },
|
||||
): Promise<WorkerReply> {
|
||||
if (this.started) throw new Error("一次性 worker 连接不能重复请求");
|
||||
this.started = true;
|
||||
if (this.terminating !== undefined) return Promise.reject(this.terminating);
|
||||
if (!this.pid)
|
||||
return this.child.status.then(
|
||||
() => {
|
||||
this.cleanup();
|
||||
throw new WorkerTransportError("exit", "worker 未启动");
|
||||
},
|
||||
(error) => {
|
||||
this.cleanup();
|
||||
throw new WorkerTransportError("spawn", "worker 子进程无法启动", error);
|
||||
},
|
||||
);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.reject = reject;
|
||||
const errPump = this.readStderr();
|
||||
let final: WorkerReply | undefined;
|
||||
const pump = (async () => {
|
||||
for await (const line of workerLines(this.stdout, options.maxResponseBytes)) {
|
||||
const msg = decodeMessage(line, req.id);
|
||||
if (final) throw new WorkerTransportError("protocol", "worker 结果之后仍发送消息");
|
||||
if (msg.type === "progress") options.onProgress?.(msg.value);
|
||||
else final = msg;
|
||||
}
|
||||
})();
|
||||
pump.catch((error) => this.terminate(this.transportError(error)));
|
||||
this.child.status.then(
|
||||
async (status) => {
|
||||
try {
|
||||
await Promise.all([pump, errPump]);
|
||||
if (this.settled) return;
|
||||
if (this.terminating !== undefined) return this.fail(this.terminating);
|
||||
if (!status.success || !final)
|
||||
throw new WorkerTransportError(
|
||||
"exit",
|
||||
`worker 异常退出(code=${status.code} signal=${status.signal}):${this.stderr}`,
|
||||
);
|
||||
this.cleanup();
|
||||
resolve(final);
|
||||
} catch (error) {
|
||||
this.fail(this.terminating ?? this.transportError(error));
|
||||
}
|
||||
},
|
||||
(error) => this.fail(new WorkerTransportError("spawn", "worker 子进程无法启动", error)),
|
||||
);
|
||||
(async () => {
|
||||
const bytes = encodeWorkerMessage(req, options.maxRequestBytes);
|
||||
const writer = this.child.stdin.getWriter();
|
||||
try {
|
||||
await writer.write(bytes);
|
||||
await writer.close();
|
||||
} finally {
|
||||
writer.releaseLock();
|
||||
}
|
||||
})().catch((error) => this.terminate(this.transportError(error)));
|
||||
if (this.terminating !== undefined) this.fail(this.terminating);
|
||||
});
|
||||
}
|
||||
|
||||
private transportError(error: unknown): WorkerTransportError {
|
||||
return error instanceof WorkerTransportError
|
||||
? error
|
||||
: new WorkerTransportError("protocol", `worker 通信失败:${(error as Error).message}`, error);
|
||||
}
|
||||
|
||||
private async readStderr(): Promise<void> {
|
||||
const decoder = new TextDecoder();
|
||||
try {
|
||||
for (;;) {
|
||||
const { done, value } = await this.stderrReader.read();
|
||||
if (done) return;
|
||||
this.stderr = (this.stderr + decoder.decode(value, { stream: true })).slice(-2000);
|
||||
}
|
||||
} catch {
|
||||
// 终止时管道可能断开
|
||||
}
|
||||
}
|
||||
|
||||
private fail(error: unknown): void {
|
||||
if (this.settled) return;
|
||||
this.cleanup();
|
||||
this.reject?.(error);
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
this.settled = true;
|
||||
clearTimeout(this.killTimer);
|
||||
void this.stdout.cancel().catch(() => {});
|
||||
void this.stderrReader.cancel().catch(() => {});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user