From d6d6918d1ba10ee765bff99982b3c97ab94a1577 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Mon, 16 Jun 2025 21:58:20 -0700 Subject: [PATCH] Safe send (#421) --- src/ipc/handlers/app_handlers.ts | 7 +++--- src/ipc/handlers/chat_stream_handlers.ts | 20 +++++++++------- src/ipc/handlers/supabase_handlers.ts | 3 ++- src/ipc/handlers/testing_chat_handlers.ts | 4 +++- src/ipc/utils/safe_sender.ts | 29 +++++++++++++++++++++++ 5 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 src/ipc/utils/safe_sender.ts diff --git a/src/ipc/handlers/app_handlers.ts b/src/ipc/handlers/app_handlers.ts index 47ea8fb..3ac7b8d 100644 --- a/src/ipc/handlers/app_handlers.ts +++ b/src/ipc/handlers/app_handlers.ts @@ -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, diff --git a/src/ipc/handlers/chat_stream_handlers.ts b/src/ipc/handlers/chat_stream_handlers.ts index db8ee62..88a0de1 100644 --- a/src/ipc/handlers/chat_stream_handlers.ts +++ b/src/ipc/handlers/chat_stream_handlers.ts @@ -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); diff --git a/src/ipc/handlers/supabase_handlers.ts b/src/ipc/handlers/supabase_handlers.ts index cd78426..c52045b 100644 --- a/src/ipc/handlers/supabase_handlers.ts +++ b/src/ipc/handlers/supabase_handlers.ts @@ -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", }); diff --git a/src/ipc/handlers/testing_chat_handlers.ts b/src/ipc/handlers/testing_chat_handlers.ts index 0a0fe81..13fe746 100644 --- a/src/ipc/handlers/testing_chat_handlers.ts +++ b/src/ipc/handlers/testing_chat_handlers.ts @@ -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 = { @@ -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, diff --git a/src/ipc/utils/safe_sender.ts b/src/ipc/utils/safe_sender.ts new file mode 100644 index 0000000..8d66290 --- /dev/null +++ b/src/ipc/utils/safe_sender.ts @@ -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}`, + ); + } +}