openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
813 lines (812 loc) • 29.7 kB
JavaScript
import { i as extractErrorCode, u as toErrorObject } from "./error-coercion-CvJK4lmH.mjs";
import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs";
import { t as killProcessTree } from "./kill-tree-ecObdMeW.mjs";
import { o as isPidDefinitelyDead } from "./pid-alive-CPS_shDt.mjs";
import { i as resolveRuntimeWorkerUrl, n as resolveRuntimeWorkerArgv } from "./runtime-worker-url-DoBSDhBS.mjs";
import { n as runtimeProcessEntrypoints } from "./runtime-process-entrypoints-Dgz2IR4T.mjs";
import { t as SpawnBrokerError } from "./protocol-foRdUdX7.mjs";
import { n as holdPipe, r as restoreStdinPipe, t as BrokerChild } from "./child-ISZYn-A7.mjs";
import { c as createBrokerSender, n as SPAWN_BROKER_STARTUP_TIMEOUT_MS, r as decodeNativeWorkerFailure, s as createBrokerReceiver, t as BrokerNativeResourceCloseError } from "./resource-protocol-DwwJYD9n.mjs";
import { chmodSync, mkdtempSync, rmSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { deserialize, serialize } from "node:v8";
import { join } from "node:path";
import { AsyncLocalStorage } from "node:async_hooks";
import { tmpdir } from "node:os";
import { spawn } from "node:child_process";
import { Socket } from "node:net";
import { randomBytes } from "node:crypto";
import { setTimeout as setTimeout$1 } from "node:timers/promises";
//#region src/process/supervisor/cancellation-policy.ts
const GRACEFUL_CANCEL_TIMEOUT_MS = 5e3;
//#endregion
//#region src/process/spawn-broker/cleanup.ts
function groupGone(pid) {
try {
process.kill(-pid, 0);
return false;
} catch (error) {
return extractErrorCode(error) === "ESRCH";
}
}
/** Preserve a lost broker's tree cleanup across host restart and process exit. */
function terminateLostBrokerChild(pid, detached, ownerExited) {
const termination = killProcessTree(pid, {
detached,
graceMs: GRACEFUL_CANCEL_TIMEOUT_MS
});
const graceEndsAt = Date.now() + GRACEFUL_CANCEL_TIMEOUT_MS;
return {
force: () => termination?.force(),
settled: (async () => {
let deadline = graceEndsAt + 2e3;
if (ownerExited) {
await ownerExited;
deadline = Math.max(deadline, Date.now() + 2e3);
}
if (!detached && Date.now() < graceEndsAt) await setTimeout$1(graceEndsAt - Date.now());
for (;;) {
if (isPidDefinitelyDead(pid) && (!detached || groupGone(pid))) break;
if (Date.now() >= deadline) throw new SpawnBrokerError(`Spawn broker child cleanup could not be confirmed for pid=${pid}`);
await setTimeout$1(50);
}
})()
};
}
/** A broker's private group retains non-detached children before PID publication. */
function terminateBrokerProcessGroup(pgid) {
let retired = false;
let signalError;
let forceTimer;
const signal = (value) => {
try {
process.kill(-pgid, value);
} catch (error) {
if (extractErrorCode(error) !== "ESRCH") signalError = error;
}
};
const retire = () => {
retired = true;
clearTimeout(forceTimer);
};
const force = () => {
if (retired) return;
if (groupGone(pgid)) {
retire();
return;
}
clearTimeout(forceTimer);
signal("SIGKILL");
};
return {
force,
settled: (async () => {
if (groupGone(pgid)) {
retire();
return;
}
signal("SIGTERM");
forceTimer = setTimeout(force, GRACEFUL_CANCEL_TIMEOUT_MS);
const deadline = Date.now() + GRACEFUL_CANCEL_TIMEOUT_MS + 2e3;
try {
while (!groupGone(pgid)) {
if (Date.now() >= deadline) throw new SpawnBrokerError(`Spawn broker group cleanup could not be confirmed for pgid=${pgid}`, { cause: signalError });
await setTimeout$1(50);
}
} finally {
retire();
}
})()
};
}
//#endregion
//#region src/process/spawn-broker/resource-host.ts
/** Captured claims are part of SpawnBrokerHost; transport loss never retires their custody. */
var BrokerResourceClaims = class {
constructor(host) {
this.host = host;
this.claims = /* @__PURE__ */ new Map();
this.bufferedBytes = 0;
this.bufferedMessages = 0;
this.closeSequence = 0;
this.sealed = false;
}
get size() {
return this.claims.size;
}
get hasOpenClaims() {
return [...this.claims.values()].some((claim) => !claim.closed);
}
capture(attachment, callbacks) {
if (this.failure || this.sealed) throw this.failure ?? new SpawnBrokerError("Native resource source is sealed");
const serializedAttachment = serialize(attachment);
if (serializedAttachment.length > 268435456) throw new SpawnBrokerError("Native resource input exceeds IPC capacity");
const captured = deserialize(serializedAttachment);
const claim = {
attachment: captured,
callbacks,
inContext: AsyncLocalStorage.snapshot(),
attached: createDeferredCore(),
ready: false,
created: false,
closed: false,
lastSequence: 0,
ownerSequence: 0,
ownerMessages: /* @__PURE__ */ new Map(),
buffered: /* @__PURE__ */ new Map()
};
claim.attached.promise.catch(() => {});
this.claims.set(captured.id, claim);
this.host.refreshReference();
const active = () => {
if (this.claims.get(captured.id) !== claim) throw new SpawnBrokerError("Native resource claim is released");
if (claim.failure) throw claim.failure;
};
return {
attachment: captured,
receive: (response) => {
if (response.id !== captured.id) throw new SpawnBrokerError("Native resource response belongs to another claim");
this.receive(response);
},
ownerMessage: (value) => {
active();
if (claim.closed) throw new SpawnBrokerError("Native resource is closed");
const sequence = claim.ownerSequence + 1;
if (!Number.isSafeInteger(sequence)) throw new SpawnBrokerError("Native resource owner sequence exhausted");
const serialized = serialize({
type: "resource-owner",
id: captured.id,
sequence,
value
});
if (this.bufferedBytes + serialized.length > 268435456 || this.bufferedMessages >= 1024) throw new SpawnBrokerError("Native resource owner delivery capacity exceeded");
claim.ownerSequence = sequence;
const completion = createDeferredCore();
completion.promise.catch(() => {});
const request = deserialize(serialized);
claim.ownerMessages.set(sequence, {
request,
size: serialized.length,
completion
});
this.bufferedBytes += serialized.length;
this.bufferedMessages++;
(async () => {
await claim.attached.promise;
active();
const pending = claim.ownerMessages.get(sequence);
if (!claim.closed && pending) await this.host.transmit(pending.request);
})().catch((cause) => {
const error = toErrorObject(cause, "Native resource owner delivery failed");
completion.reject(error);
if (this.claims.get(captured.id) === claim && !claim.closed && !claim.failure) claim.inContext(() => claim.callbacks.failed(error));
});
return {
sequence,
result: completion.promise
};
},
close: () => {
if (claim.closed) return Promise.resolve();
try {
active();
} catch (error) {
return Promise.reject(toErrorObject(error, "Native resource is unavailable"));
}
if (claim.pending) return claim.pending.completion.promise;
const requestId = --this.closeSequence;
if (!Number.isSafeInteger(requestId)) return Promise.reject(new SpawnBrokerError("Native resource close sequence exhausted"));
const completion = createDeferredCore();
completion.promise.catch(() => {});
claim.pending = {
requestId,
completion
};
(async () => {
await claim.attached.promise;
active();
if (!claim.closed && claim.pending?.requestId === requestId) await this.host.transmit({
type: "resource-close",
id: captured.id,
requestId
});
})().catch((error) => {
if (claim.pending?.requestId === requestId) {
claim.pending = void 0;
completion.reject(toErrorObject(error, "Native resource close delivery failed"));
}
});
return completion.promise;
},
release: () => {
if (this.claims.get(captured.id) !== claim) return;
if (!claim.closed) throw new SpawnBrokerError("Native resource must close before release");
this.host.transmit({
type: "resource-release",
id: captured.id
}).catch(() => {});
this.remove(claim);
},
abandonUnattached: () => {
if (this.claims.get(captured.id) !== claim) return;
if (claim.ready || claim.created) throw new SpawnBrokerError("Attached native resource cannot be abandoned");
const error = new SpawnBrokerError("Native resource was never dispatched");
claim.attached.reject(error);
claim.pending?.completion.reject(error);
this.remove(claim, error);
}
};
}
receive(response) {
const claim = this.claims.get(response.id);
if (!claim || claim.failure && response.type !== "resource-closed") return;
if (response.type === "resource-ready") {
this.host.markReady(response.pid, response.generation);
if (claim.ready) return;
claim.ready = true;
claim.attached.resolve();
} else if (response.type === "resource-created") {
if (claim.created) return;
claim.created = true;
} else if (response.type === "resource-owner-received" || response.type === "resource-owner-rejected") {
const pending = claim.ownerMessages.get(response.sequence);
if (pending) {
claim.ownerMessages.delete(response.sequence);
this.bufferedBytes -= pending.size;
this.bufferedMessages--;
if (response.type === "resource-owner-rejected") {
const error = toErrorObject(decodeNativeWorkerFailure(response.error), "Native resource owner callback failed");
pending.completion.reject(error);
claim.inContext(() => claim.callbacks.failed(error));
} else pending.completion.resolve();
}
return;
} else if (response.type === "resource-owner") {
if (!Number.isSafeInteger(response.sequence) || response.sequence <= 0) {
this.failClaim(claim, new SpawnBrokerError("Invalid native resource owner sequence"));
return;
}
if (response.sequence <= claim.lastSequence || claim.buffered.has(response.sequence)) return;
if (response.sequence !== claim.lastSequence + 1) {
const size = serialize(response).length;
if (this.bufferedBytes + size > 268435456 || this.bufferedMessages >= 1024) {
this.failClaim(claim, new SpawnBrokerError("Native resource receive capacity exceeded"));
return;
}
claim.buffered.set(response.sequence, {
response,
size
});
this.bufferedBytes += size;
this.bufferedMessages++;
return;
}
claim.lastSequence = response.sequence;
claim.inContext(() => claim.callbacks.message(response));
for (;;) {
const next = claim.buffered.get(claim.lastSequence + 1);
if (!next) break;
claim.buffered.delete(++claim.lastSequence);
this.bufferedBytes -= next.size;
this.bufferedMessages--;
claim.inContext(() => claim.callbacks.message(next.response));
}
return;
} else if (response.type === "resource-closed") {
if (claim.closed) return;
claim.closed = true;
this.clearOwnerMessages(claim, new SpawnBrokerError("Native resource closed before owner delivery was acknowledged"));
claim.pending?.completion.resolve();
claim.pending = void 0;
this.host.refreshReference();
} else if (response.type === "resource-close-error") {
if (claim.pending?.requestId === response.requestId) {
const pending = claim.pending;
claim.pending = void 0;
pending.completion.reject(response.resourceError ? new BrokerNativeResourceCloseError(response.error) : toErrorObject(decodeNativeWorkerFailure(response.error), "Native resource cleanup failed"));
}
} else if (response.type === "resource-failed") {
const error = toErrorObject(decodeNativeWorkerFailure(response.error), "Native resource failed");
claim.pending?.completion.reject(error);
claim.pending = void 0;
for (const pending of claim.ownerMessages.values()) pending.completion.reject(error);
claim.inContext(() => claim.callbacks.failed(error));
return;
}
claim.inContext(() => claim.callbacks.message(response));
}
seal() {
if (this.sealPromise) return this.sealPromise;
this.sealed = true;
this.sealPromise = this.host.transmit({ type: "resource-seal" }).then(() => {
for (const claim of this.claims.values()) claim.attached.resolve();
});
return this.sealPromise;
}
fail(error) {
this.failure ??= error;
const failed = [...this.claims.values()].filter((claim) => this.rejectClaim(claim, error));
const callbackErrors = [];
for (const claim of failed) try {
claim.inContext(() => claim.callbacks.failed(error));
} catch (callbackError) {
callbackErrors.push(callbackError);
}
if (callbackErrors.length) throw new AggregateError(callbackErrors, "Native resource failure callback failed");
}
failClaim(claim, error) {
if (this.rejectClaim(claim, error)) claim.inContext(() => claim.callbacks.failed(error));
}
rejectClaim(claim, error) {
if (claim.failure || claim.closed) return false;
claim.failure = error;
claim.attached.reject(error);
claim.pending?.completion.reject(error);
for (const pending of claim.ownerMessages.values()) pending.completion.reject(error);
claim.pending = void 0;
this.clearBuffered(claim);
return true;
}
clearBuffered(claim) {
for (const value of claim.buffered.values()) this.bufferedBytes -= value.size;
this.bufferedMessages -= claim.buffered.size;
claim.buffered.clear();
}
clearOwnerMessages(claim, error) {
for (const pending of claim.ownerMessages.values()) {
this.bufferedBytes -= pending.size;
this.bufferedMessages--;
pending.completion.reject(error);
}
claim.ownerMessages.clear();
}
remove(claim, error) {
this.clearBuffered(claim);
this.clearOwnerMessages(claim, error ?? new SpawnBrokerError("Native resource released before owner delivery was acknowledged"));
this.claims.delete(claim.attachment.id);
this.host.refreshReference();
}
};
//#endregion
//#region src/process/spawn-broker/host.ts
const MAX_REQUESTS = 256;
const RESTART_DELAYS = [
100,
250,
500,
1e3,
2e3
];
const UNIX_SOCKET_PATH_BYTES = 103;
const MAX_BOOTSTRAP_BYTES = 1024;
function createNativeResourceDirectory() {
const directory = mkdtempSync(join(tmpdir(), "oc-br-"));
if (process.platform === "win32" || Buffer.byteLength(join(directory, "resource.sock")) <= UNIX_SOCKET_PATH_BYTES) return directory;
rmSync(directory, {
recursive: true,
force: true
});
return mkdtempSync("/tmp/oc-br-");
}
function brokerSpawnOptions(options) {
const stdio = options.stdio ?? "pipe";
const entries = typeof stdio === "string" ? [
stdio,
stdio,
stdio
] : [...stdio];
const normalized = [];
for (let fd = 0; fd < Math.max(3, entries.length); fd += 1) {
const entry = entries[fd] ?? (fd < 3 ? "pipe" : "ignore");
if (entry === "inherit" && fd !== 0) return;
if (entry !== "pipe" && entry !== "ignore" && entry !== "inherit" && entry !== "ipc") return;
normalized.push(entry);
}
if (options.signal || options.timeout || options.killSignal) throw new Error("Unsupported spawn broker cancellation options");
return {
cwd: options.cwd instanceof URL ? fileURLToPath(options.cwd) : options.cwd,
env: options.env ? { ...options.env } : { ...process.env },
argv0: options.argv0,
detached: options.detached,
shell: options.shell,
windowsHide: options.windowsHide,
windowsVerbatimArguments: options.windowsVerbatimArguments,
serialization: options.serialization,
uid: options.uid,
gid: options.gid,
stdio: normalized
};
}
var SpawnBrokerHost = class {
constructor(options = {}) {
this.options = options;
this.runInContext = AsyncLocalStorage.snapshot();
this.readiness = createDeferredCore();
this.available = false;
this.hasBeenReady = false;
this.closing = false;
this.generation = 0;
this.consecutiveFailures = 0;
this.sequence = 0;
this.requests = /* @__PURE__ */ new Map();
this.cleanups = /* @__PURE__ */ new Set();
this.cleanupErrors = [];
this.startupDeadline = 0;
this.onParentExit = () => {
if (this.process?.connected) this.process.disconnect();
for (const cleanup of this.cleanups) cleanup.force();
};
this.workerUrl = options.workerUrl ?? resolveRuntimeWorkerUrl(runtimeProcessEntrypoints.spawnBroker);
this.entryPath = fileURLToPath(this.workerUrl);
if (options.nativeResources) {
this.resourceDirectory = createNativeResourceDirectory();
chmodSync(this.resourceDirectory, 448);
const secret = randomBytes(32).toString("hex");
this.resourceAuthority = {
secret,
endpoint: process.platform === "win32" ? `\\\\.\\pipe\\oc-br-${secret}` : join(this.resourceDirectory, "resource.sock")
};
this.resourceClaims = new BrokerResourceClaims({
transmit: async (request) => {
if (!this.available) await this.ready();
await this.transmit(request);
},
markReady: (pid, generation) => {
if (!this.markNativeReady) throw new SpawnBrokerError("Native resource broker unavailable");
this.markNativeReady(pid, generation);
},
refreshReference: () => this.refreshNativeReference()
});
}
this.readiness.promise.catch(() => {});
try {
this.start();
} catch (error) {
if (this.resourceDirectory) rmSync(this.resourceDirectory, {
recursive: true,
force: true
});
throw error;
}
process.once("exit", this.onParentExit);
}
get pid() {
return this.process?.pid;
}
ready() {
return this.closing ? Promise.reject(new SpawnBrokerError("Spawn broker is closing")) : this.readiness.promise;
}
captureNativeResource(input, callbacks) {
if (!this.resourceClaims || !this.resourceAuthority || this.closing || this.requests.size + this.resourceClaims.size >= MAX_REQUESTS) throw new SpawnBrokerError("Native resource broker is unavailable or at capacity");
return this.resourceClaims.capture({
...input,
...this.resourceAuthority,
generation: this.generation - 1,
startupDeadline: this.available ? void 0 : this.startupDeadline,
id: ++this.sequence
}, callbacks);
}
sealNativeResources() {
if (this.resourceClaims && !this.available) return Promise.reject(new SpawnBrokerError("Native resource broker is not ready for cleanup"));
return this.resourceClaims?.seal() ?? Promise.reject(new SpawnBrokerError("Native resources are disabled"));
}
serviceNativeResources() {
this.serviceStartup?.();
}
refreshNativeReference() {
if (!this.resourceClaims || !this.process) return;
if (this.closing || this.resourceClaims.hasOpenClaims || this.requests.size > 0) {
this.process.ref();
this.process.channel?.ref?.();
} else {
this.process.unref();
this.process.channel?.unref?.();
}
}
spawn(command, args, options) {
const prepared = brokerSpawnOptions(options);
if (!prepared) throw new Error("Unsupported spawn broker stdio or process options");
return this.admit({
type: "spawn",
id: ++this.sequence,
argv: [command, ...args],
options: prepared
}).child;
}
spawnExeca(argv, options) {
const id = ++this.sequence;
const request = this.admit({
type: "spawn-execa",
id,
argv,
options
});
return {
child: request.child,
result: request.result.promise,
cancel: () => {
this.transmit({
type: "cancel",
id
}).catch((error) => request.child.fail(toErrorObject(error, "Spawn broker cancellation delivery failed")));
}
};
}
admit(message) {
const child = new BrokerChild(message.id, message.argv, (value, handle) => this.transmit(value, handle));
const result = message.type === "spawn-execa" ? createDeferredCore() : void 0;
if (result) result.promise.catch(() => {});
const request = {
child,
result,
childClosed: false,
resultSettled: !result,
detached: message.options.detached === true || message.type === "spawn-execa" && message.options.killDescendants === true
};
const fail = (error) => {
result?.reject(error);
child.fail(error);
};
if (!this.available || this.closing || this.requests.size + (this.resourceClaims?.size ?? 0) >= MAX_REQUESTS) {
child.markNotStarted();
queueMicrotask(() => fail(new SpawnBrokerError("Spawn broker is unavailable")));
return request;
}
this.requests.set(message.id, request);
this.refreshNativeReference();
child.waitForClose().then(() => {
request.childClosed = true;
this.retire(message.id, request);
});
this.transmit(message).catch((error) => {
this.requests.delete(message.id);
this.refreshNativeReference();
fail(new SpawnBrokerError("Spawn broker request delivery failed", { cause: error }));
});
return request;
}
retire(id, request) {
if (request.childClosed && request.resultSettled) {
this.requests.delete(id);
this.refreshNativeReference();
}
}
retainCleanup(cleanup) {
this.cleanups.add(cleanup);
cleanup.settled.then(() => this.cleanups.delete(cleanup), (failure) => {
this.cleanupErrors.push(toErrorObject(failure, "Spawn broker cleanup failed"));
this.cleanups.delete(cleanup);
});
}
transmit(message, handle) {
if (!this.available || !this.sendMessage) return Promise.reject(new SpawnBrokerError("Spawn broker is unavailable"));
return this.sendMessage(message, handle);
}
start() {
if (this.closing) return;
const generation = this.generation++;
const bootstrap = {
type: "bootstrap",
...this.resourceAuthority ? { nativeResource: {
...this.resourceAuthority,
generation
} } : {}
};
if (serialize(bootstrap).byteLength > MAX_BOOTSTRAP_BYTES) throw new SpawnBrokerError("Spawn broker bootstrap exceeds its IPC bound");
const child = spawn(process.execPath, resolveRuntimeWorkerArgv(this.workerUrl), {
stdio: [
"inherit",
"ignore",
"ignore",
"ipc"
],
detached: true,
serialization: "advanced"
});
this.process = child;
this.brokerClosed = new Promise((resolve) => {
child.once("close", () => resolve());
});
const receiver = createBrokerReceiver();
const brokerExited = createDeferredCore();
let ended = false;
let ready = false;
this.startupDeadline = Date.now() + SPAWN_BROKER_STARTUP_TIMEOUT_MS;
const checkStartup = () => {
if (!ended && !ready && Date.now() >= this.startupDeadline) {
fail(/* @__PURE__ */ new Error("readiness deadline exceeded after 15000ms"));
child.kill("SIGKILL");
}
};
const startupTimer = this.resourceClaims ? void 0 : setTimeout(checkStartup, SPAWN_BROKER_STARTUP_TIMEOUT_MS);
this.serviceStartup = checkStartup;
const fail = (cause) => {
if (ended) return;
ended = true;
clearTimeout(startupTimer);
this.markNativeReady = void 0;
receiver.clear();
this.available = false;
this.sendMessage = void 0;
const error = new SpawnBrokerError(this.hasBeenReady ? "Spawn broker exited; command outcome is unavailable" : `Spawn broker failed before readiness: ${cause?.message ?? "channel lost"}`, { cause });
const previousReadiness = this.readiness;
this.readiness = createDeferredCore();
this.readiness.promise.catch(() => {});
previousReadiness.reject(error);
for (const request of this.requests.values()) {
if (request.pid && !request.childClosed) {
const cleanup = terminateLostBrokerChild(request.pid, request.detached, this.closing ? brokerExited.promise : void 0);
this.retainCleanup(cleanup);
}
request.result?.reject(error);
request.child.fail(error);
}
this.requests.clear();
this.refreshNativeReference();
if (child.pid && process.platform !== "win32") this.retainCleanup(terminateBrokerProcessGroup(child.pid));
if (this.resourceClaims || this.closing || !this.hasBeenReady) {
this.readiness.reject(error);
this.resourceClaims?.fail(error);
return;
}
const delay = RESTART_DELAYS[this.consecutiveFailures++];
if (delay === void 0) {
this.readiness.reject(error);
return;
}
this.restartTimer = setTimeout(() => this.start(), delay);
};
const markReady = (pid, receivedGeneration) => {
if (ended || pid !== child.pid || receivedGeneration !== generation) throw new SpawnBrokerError("Native resource readiness does not match the captured broker");
if (ready) return;
ready = true;
clearTimeout(startupTimer);
this.hasBeenReady = true;
this.consecutiveFailures = 0;
this.available = true;
this.readiness.resolve();
this.runInContext(() => this.options.onReady?.(pid, generation > 0));
};
this.markNativeReady = markReady;
child.once("error", fail);
child.once("exit", (code, signal) => {
brokerExited.resolve();
fail(/* @__PURE__ */ new Error(`exited with code=${code ?? "null"} signal=${signal ?? "none"}`));
});
child.once("disconnect", () => fail(/* @__PURE__ */ new Error("IPC channel disconnected")));
const abortTransport = (error) => {
fail(error);
if (!this.resourceClaims && child.connected) child.disconnect();
child.kill("SIGTERM");
};
child.on("message", (raw, handle) => {
if (ended || this.closing) {
if (handle instanceof Socket) handle.destroy();
return;
}
let decoded;
try {
decoded = receiver.receive(raw);
} catch (error) {
abortTransport(error instanceof Error ? error : new Error(String(error)));
return;
}
if (decoded === void 0) return;
const message = decoded;
if (message.type === "ready") {
markReady(message.pid, generation);
return;
}
if (message.type === "resource-ready" || message.type === "resource-created" || message.type === "resource-target" || message.type === "resource-owner" || message.type === "resource-owner-received" || message.type === "resource-owner-rejected" || message.type === "resource-closed" || message.type === "resource-close-error" || message.type === "resource-failed") {
this.resourceClaims?.receive(message);
return;
}
const request = this.requests.get(message.id);
if (!request) {
if (handle instanceof Socket) handle.destroy();
if (message.type === "pipe") this.transmit({
type: "pipe-received",
id: message.id,
fd: message.fd
}).catch(fail);
return;
}
if (message.type === "owned") request.pid = message.pid;
else if (message.type === "pipe") {
try {
if (message.closed && message.fd === 0) {
const stdin = new Socket();
request.child.attachPipe(message.fd, stdin);
stdin.destroy();
} else if (handle instanceof Socket) {
if (message.fd === 0) restoreStdinPipe(handle);
else holdPipe(handle);
request.child.attachPipe(message.fd, handle);
} else throw new SpawnBrokerError("Spawn broker pipe transfer failed");
} catch (error) {
if (handle instanceof Socket) handle.destroy();
abortTransport(toErrorObject(error, "Spawn broker pipe setup failed"));
return;
}
this.transmit({
type: "pipe-received",
id: message.id,
fd: message.fd
}).catch(fail);
} else if (message.type === "pipe-prefix") {
const pipe = request.child.stdio[message.fd];
if (pipe instanceof Socket && message.bytes.length > 0) pipe.unshift(message.bytes);
} else if (message.type === "execa-result") {
if (request.pid === void 0 && message.result.failed) request.child.markNotStarted();
request.result?.resolve(message.result);
request.resultSettled = true;
this.retire(message.id, request);
} else {
if (message.type === "error" && message.resultUnavailable && request.result) {
request.result.reject(Object.assign(new Error(message.error.message), message.error));
request.resultSettled = true;
}
request.child.receive(message);
this.retire(message.id, request);
}
});
try {
child.send(bootstrap, (error) => {
if (error) abortTransport(error);
});
} catch (error) {
abortTransport(toErrorObject(error, "Spawn broker bootstrap delivery failed"));
}
if (!ended) this.sendMessage = createBrokerSender((message, handle, callback) => child.send(message, handle, { keepOpen: true }, callback));
this.refreshNativeReference();
}
/** Join cleanup already retained by this host, including recorded failures. */
async waitForCleanup() {
await Promise.allSettled([...this.cleanups].map((cleanup) => cleanup.settled));
if (this.cleanupErrors.length) throw new AggregateError(this.cleanupErrors, "Spawn broker cleanup did not complete");
}
close() {
if (this.resourceClaims?.hasOpenClaims) return Promise.reject(new SpawnBrokerError("Native resource claims must close before broker shutdown"));
return this.closePromise ??= this.closeInternal();
}
async closeInternal() {
this.closing = true;
this.refreshNativeReference();
this.readiness.reject(new SpawnBrokerError("Spawn broker is closing"));
clearTimeout(this.restartTimer);
try {
const child = this.process;
if (child && child.exitCode === null && child.signalCode === null) {
const exited = this.resourceClaims && this.brokerClosed ? this.brokerClosed : new Promise((resolve) => {
child.once("exit", () => resolve());
});
const timer = setTimeout(() => child.kill("SIGKILL"), 7e3);
try {
if (this.resourceClaims) this.transmit({ type: "shutdown" }).catch(() => child.kill("SIGTERM"));
else if (child.connected) child.disconnect();
await exited;
} finally {
clearTimeout(timer);
}
}
try {
await this.waitForCleanup();
} finally {
if (this.resourceClaims) {
await this.brokerClosed;
if (this.resourceDirectory) rmSync(this.resourceDirectory, {
recursive: true,
force: true
});
}
}
} finally {
process.removeListener("exit", this.onParentExit);
}
}
};
function createSpawnBrokerHost(options) {
return new SpawnBrokerHost(options);
}
//#endregion
export { createSpawnBrokerHost as n, GRACEFUL_CANCEL_TIMEOUT_MS as r, brokerSpawnOptions as t };