mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
fix(daemon): keep lifecycle operations on the selected instance
Restart reconstructed launch inputs and address fallback could redirect a home-selected shutdown to another daemon. Let supervisors publish bound endpoints, retain their worker restart contract, and share captured local lifecycle ownership between CLI and Desktop.
This commit is contained in:
@@ -1469,7 +1469,7 @@ test("honors explicit getDaemonStatus timeout below the session RPC default", as
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
mock.triggerOpen({ features: { daemonStatusRpc: true } });
|
||||
await connectPromise;
|
||||
|
||||
const responsePromise = client.getDaemonStatus({
|
||||
|
||||
@@ -879,6 +879,16 @@ type CorrelatedResponsePayload<TType extends CorrelatedResponseType> = Extract<
|
||||
{ type: TType }
|
||||
>["payload"];
|
||||
|
||||
export class DaemonConnectionError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly code: "DAEMON_CONNECTION_LOST" | "DAEMON_REQUEST_TIMEOUT" = "DAEMON_CONNECTION_LOST",
|
||||
) {
|
||||
super(message);
|
||||
this.name = "DaemonConnectionError";
|
||||
}
|
||||
}
|
||||
|
||||
class DaemonRpcError extends Error {
|
||||
readonly requestId: string;
|
||||
readonly requestType?: string;
|
||||
@@ -1579,7 +1589,7 @@ export class DaemonClient {
|
||||
|
||||
private sendTransportFrame(frame: string | Uint8Array | ArrayBuffer): void {
|
||||
if (!this.transport) {
|
||||
throw new Error("Transport not connected");
|
||||
throw new DaemonConnectionError("Transport not connected");
|
||||
}
|
||||
const isOpen = this.beginTraceSection("paseo.ws.frame.outbound", {
|
||||
kind: typeof frame === "string" ? "text" : "binary",
|
||||
@@ -1661,7 +1671,12 @@ export class DaemonClient {
|
||||
if (idx !== -1) {
|
||||
this.pendingSendQueue.splice(idx, 1);
|
||||
}
|
||||
reject(new Error(`Timed out waiting for connection to send message`));
|
||||
reject(
|
||||
new DaemonConnectionError(
|
||||
"Timed out waiting for connection to send message",
|
||||
"DAEMON_REQUEST_TIMEOUT",
|
||||
),
|
||||
);
|
||||
}, DEFAULT_SEND_QUEUE_TIMEOUT_MS);
|
||||
|
||||
this.pendingSendQueue.push({ message, resolve, reject, timeoutHandle });
|
||||
@@ -1669,7 +1684,7 @@ export class DaemonClient {
|
||||
}
|
||||
|
||||
// Not connected and not connecting - fail immediately
|
||||
return Promise.reject(new Error(`Transport not connected (status: ${status})`));
|
||||
return Promise.reject(new DaemonConnectionError(`Transport not connected (status: ${status})`));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1687,7 +1702,7 @@ export class DaemonClient {
|
||||
this.sendJsonMessage("session", payload.type, { type: "session", message: payload });
|
||||
pending.resolve();
|
||||
} else {
|
||||
pending.reject(new Error("Connection lost before message could be sent"));
|
||||
pending.reject(new DaemonConnectionError("Connection lost before message could be sent"));
|
||||
}
|
||||
} catch (error) {
|
||||
pending.reject(error instanceof Error ? error : new Error(String(error)));
|
||||
@@ -1834,7 +1849,7 @@ export class DaemonClient {
|
||||
|
||||
private sendSessionMessageStrict(message: SessionInboundMessage): void {
|
||||
if (!this.transport || this.connectionState.status !== "connected") {
|
||||
throw new Error("Transport not connected");
|
||||
throw new DaemonConnectionError("Transport not connected");
|
||||
}
|
||||
const payload = SessionInboundMessageSchema.parse(message);
|
||||
try {
|
||||
@@ -3437,7 +3452,11 @@ export class DaemonClient {
|
||||
return payload.notice ?? null;
|
||||
}
|
||||
|
||||
async restartServer(reason?: string, requestId?: string): Promise<RestartRequestedStatusPayload> {
|
||||
async restartServer(
|
||||
reason?: string,
|
||||
requestId?: string,
|
||||
options?: { timeout?: number },
|
||||
): Promise<RestartRequestedStatusPayload> {
|
||||
const resolvedRequestId = this.createRequestId(requestId);
|
||||
const message = SessionInboundMessageSchema.parse({
|
||||
type: "restart_server_request",
|
||||
@@ -3447,6 +3466,7 @@ export class DaemonClient {
|
||||
return this.sendRequest({
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
timeout: options?.timeout,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "status") {
|
||||
@@ -4819,6 +4839,10 @@ export class DaemonClient {
|
||||
}
|
||||
|
||||
async getDaemonStatus(options?: DaemonStatusOptions): Promise<DaemonStatusPayload> {
|
||||
if (!this.lastServerInfoMessage) throw new DaemonConnectionError("Transport not connected");
|
||||
if (this.lastServerInfoMessage?.features?.daemonStatusRpc !== true) {
|
||||
throw new Error("Update the host to read daemon status.");
|
||||
}
|
||||
return this.sendCorrelatedSessionRequest({
|
||||
requestId: options?.requestId,
|
||||
message: {
|
||||
@@ -6115,9 +6139,9 @@ export class DaemonClient {
|
||||
|
||||
// Clear all pending waiters and queued sends since the connection was lost
|
||||
// and responses from the previous connection will never arrive.
|
||||
this.clearWaiters(new Error(reason ?? "Connection lost"));
|
||||
this.rejectPendingSendQueue(new Error(reason ?? "Connection lost"));
|
||||
this.rejectPingProbe(new Error(reason ?? "Connection lost"));
|
||||
this.clearWaiters(new DaemonConnectionError(reason ?? "Connection lost"));
|
||||
this.rejectPendingSendQueue(new DaemonConnectionError(reason ?? "Connection lost"));
|
||||
this.rejectPingProbe(new DaemonConnectionError(reason ?? "Connection lost"));
|
||||
this.terminalStreams.clearSlots();
|
||||
this.lastServerInfoMessage = null;
|
||||
|
||||
@@ -6392,7 +6416,10 @@ export class DaemonClient {
|
||||
options?: WaitOptions,
|
||||
): WaitHandle<T> {
|
||||
// Capture stack trace at call site, not inside setTimeout
|
||||
const timeoutError = new Error(`Timeout waiting for message (${timeout}ms)`);
|
||||
const timeoutError = new DaemonConnectionError(
|
||||
`Timeout waiting for message (${timeout}ms)`,
|
||||
"DAEMON_REQUEST_TIMEOUT",
|
||||
);
|
||||
|
||||
let waiter: Waiter<T> | null = null;
|
||||
let settled = false;
|
||||
|
||||
Reference in New Issue
Block a user