Safe send (#421)

This commit is contained in:
Will Chen
2025-06-16 21:58:20 -07:00
committed by GitHub
parent 30b5c0d0ef
commit d6d6918d1b
5 changed files with 50 additions and 13 deletions

View File

@@ -39,6 +39,7 @@ import { startProxy } from "../utils/start_proxy_server";
import { Worker } from "worker_threads";
import { createFromTemplate } from "./createFromTemplate";
import { gitCommit } from "../utils/git_utils";
import { safeSend } from "../utils/safe_sender";
async function copyDir(
source: string,
@@ -126,7 +127,7 @@ async function executeAppLocalNode({
const message = util.stripVTControlCharacters(data.toString());
logger.debug(`App ${appId} (PID: ${process.pid}) stdout: ${message}`);
event.sender.send("app:output", {
safeSend(event.sender, "app:output", {
type: "stdout",
message,
appId,
@@ -135,7 +136,7 @@ async function executeAppLocalNode({
if (urlMatch) {
proxyWorker = await startProxy(urlMatch[1], {
onStarted: (proxyUrl) => {
event.sender.send("app:output", {
safeSend(event.sender, "app:output", {
type: "stdout",
message: `[dyad-proxy-server]started=[${proxyUrl}] original=[${urlMatch[1]}]`,
appId,
@@ -148,7 +149,7 @@ async function executeAppLocalNode({
process.stderr?.on("data", (data) => {
const message = util.stripVTControlCharacters(data.toString());
logger.error(`App ${appId} (PID: ${process.pid}) stderr: ${message}`);
event.sender.send("app:output", {
safeSend(event.sender, "app:output", {
type: "stderr",
message,
appId,

View File

@@ -37,6 +37,8 @@ import { GoogleGenerativeAIProviderOptions } from "@ai-sdk/google";
import { getExtraProviderOptions } from "../utils/thinking_utils";
import { safeSend } from "../utils/safe_sender";
const logger = log.scope("chat_stream_handlers");
// Track active streams for cancellation
@@ -237,7 +239,7 @@ ${componentSnippet}
}
// Send the messages right away so that the loading state is shown for the message.
event.sender.send("chat:response:chunk", {
safeSend(event.sender, "chat:response:chunk", {
chatId: req.chatId,
messages: updatedChat.messages,
});
@@ -540,7 +542,7 @@ This conversation includes one or more image attachments. When the user uploads
}
// Update the assistant message in the database
event.sender.send("chat:response:chunk", {
safeSend(event.sender, "chat:response:chunk", {
chatId: req.chatId,
messages: currentMessages,
});
@@ -622,27 +624,28 @@ This conversation includes one or more image attachments. When the user uploads
},
});
event.sender.send("chat:response:chunk", {
safeSend(event.sender, "chat:response:chunk", {
chatId: req.chatId,
messages: chat!.messages,
});
if (status.error) {
event.sender.send(
safeSend(
event.sender,
"chat:response:error",
`Sorry, there was an error applying the AI's changes: ${status.error}`,
);
}
// Signal that the stream has completed
event.sender.send("chat:response:end", {
safeSend(event.sender, "chat:response:end", {
chatId: req.chatId,
updatedFiles: status.updatedFiles ?? false,
extraFiles: status.extraFiles,
extraFilesError: status.extraFilesError,
} satisfies ChatResponseEnd);
} else {
event.sender.send("chat:response:end", {
safeSend(event.sender, "chat:response:end", {
chatId: req.chatId,
updatedFiles: false,
} satisfies ChatResponseEnd);
@@ -674,7 +677,8 @@ This conversation includes one or more image attachments. When the user uploads
return req.chatId;
} catch (error) {
logger.error("Error calling LLM:", error);
event.sender.send(
safeSend(
event.sender,
"chat:response:error",
`Sorry, there was an error processing your request: ${error}`,
);
@@ -698,7 +702,7 @@ This conversation includes one or more image attachments. When the user uploads
}
// Send the end event to the renderer
event.sender.send("chat:response:end", {
safeSend(event.sender, "chat:response:end", {
chatId,
updatedFiles: false,
} satisfies ChatResponseEnd);

View File

@@ -8,6 +8,7 @@ import {
createTestOnlyLoggedHandler,
} from "./safe_handle";
import { handleSupabaseOAuthReturn } from "../../supabase_admin/supabase_return_handler";
import { safeSend } from "../utils/safe_sender";
const logger = log.scope("supabase_handlers");
const handle = createLoggedHandler(logger);
@@ -70,7 +71,7 @@ export function registerSupabaseHandlers() {
);
// Simulate the deep link event
event.sender.send("deep-link-received", {
safeSend(event.sender, "deep-link-received", {
type: "supabase-oauth-return",
url: "https://supabase-oauth.dyad.sh/api/connect-supabase/login",
});

View File

@@ -1,3 +1,5 @@
import { safeSend } from "../utils/safe_sender";
// e.g. [dyad-qa=add-dep]
// Canned responses for test prompts
const TEST_RESPONSES: Record<string, string> = {
@@ -64,7 +66,7 @@ export async function streamTestResponse(
fullResponse += chunk + " ";
// Send the current accumulated response
event.sender.send("chat:response:chunk", {
safeSend(event.sender, "chat:response:chunk", {
chatId: chatId,
messages: [
...updatedChat.messages,

View File

@@ -0,0 +1,29 @@
import type { WebContents } from "electron";
import log from "electron-log";
/**
* Sends an IPC message to the renderer only if the provided `WebContents` is
* still alive. This prevents `Object has been destroyed` errors that can occur
* when asynchronous callbacks attempt to communicate after the window has
* already been closed (e.g. during e2e test teardown).
*/
export function safeSend(
sender: WebContents | null | undefined,
channel: string,
...args: unknown[]
): void {
if (!sender) return;
if (sender.isDestroyed()) return;
// @ts-ignore `isCrashed` exists at runtime but is not in the type defs
if (typeof sender.isCrashed === "function" && sender.isCrashed()) return;
try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore allow variadic args beyond `data`
sender.send(channel, ...args);
} catch (error) {
log.debug(
`safeSend: failed to send on channel "${channel}" because: ${(error as Error).message}`,
);
}
}