Safe send (#421)
This commit is contained in:
@@ -39,6 +39,7 @@ import { startProxy } from "../utils/start_proxy_server";
|
|||||||
import { Worker } from "worker_threads";
|
import { Worker } from "worker_threads";
|
||||||
import { createFromTemplate } from "./createFromTemplate";
|
import { createFromTemplate } from "./createFromTemplate";
|
||||||
import { gitCommit } from "../utils/git_utils";
|
import { gitCommit } from "../utils/git_utils";
|
||||||
|
import { safeSend } from "../utils/safe_sender";
|
||||||
|
|
||||||
async function copyDir(
|
async function copyDir(
|
||||||
source: string,
|
source: string,
|
||||||
@@ -126,7 +127,7 @@ async function executeAppLocalNode({
|
|||||||
const message = util.stripVTControlCharacters(data.toString());
|
const message = util.stripVTControlCharacters(data.toString());
|
||||||
logger.debug(`App ${appId} (PID: ${process.pid}) stdout: ${message}`);
|
logger.debug(`App ${appId} (PID: ${process.pid}) stdout: ${message}`);
|
||||||
|
|
||||||
event.sender.send("app:output", {
|
safeSend(event.sender, "app:output", {
|
||||||
type: "stdout",
|
type: "stdout",
|
||||||
message,
|
message,
|
||||||
appId,
|
appId,
|
||||||
@@ -135,7 +136,7 @@ async function executeAppLocalNode({
|
|||||||
if (urlMatch) {
|
if (urlMatch) {
|
||||||
proxyWorker = await startProxy(urlMatch[1], {
|
proxyWorker = await startProxy(urlMatch[1], {
|
||||||
onStarted: (proxyUrl) => {
|
onStarted: (proxyUrl) => {
|
||||||
event.sender.send("app:output", {
|
safeSend(event.sender, "app:output", {
|
||||||
type: "stdout",
|
type: "stdout",
|
||||||
message: `[dyad-proxy-server]started=[${proxyUrl}] original=[${urlMatch[1]}]`,
|
message: `[dyad-proxy-server]started=[${proxyUrl}] original=[${urlMatch[1]}]`,
|
||||||
appId,
|
appId,
|
||||||
@@ -148,7 +149,7 @@ async function executeAppLocalNode({
|
|||||||
process.stderr?.on("data", (data) => {
|
process.stderr?.on("data", (data) => {
|
||||||
const message = util.stripVTControlCharacters(data.toString());
|
const message = util.stripVTControlCharacters(data.toString());
|
||||||
logger.error(`App ${appId} (PID: ${process.pid}) stderr: ${message}`);
|
logger.error(`App ${appId} (PID: ${process.pid}) stderr: ${message}`);
|
||||||
event.sender.send("app:output", {
|
safeSend(event.sender, "app:output", {
|
||||||
type: "stderr",
|
type: "stderr",
|
||||||
message,
|
message,
|
||||||
appId,
|
appId,
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import { GoogleGenerativeAIProviderOptions } from "@ai-sdk/google";
|
|||||||
|
|
||||||
import { getExtraProviderOptions } from "../utils/thinking_utils";
|
import { getExtraProviderOptions } from "../utils/thinking_utils";
|
||||||
|
|
||||||
|
import { safeSend } from "../utils/safe_sender";
|
||||||
|
|
||||||
const logger = log.scope("chat_stream_handlers");
|
const logger = log.scope("chat_stream_handlers");
|
||||||
|
|
||||||
// Track active streams for cancellation
|
// 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.
|
// 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,
|
chatId: req.chatId,
|
||||||
messages: updatedChat.messages,
|
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
|
// Update the assistant message in the database
|
||||||
event.sender.send("chat:response:chunk", {
|
safeSend(event.sender, "chat:response:chunk", {
|
||||||
chatId: req.chatId,
|
chatId: req.chatId,
|
||||||
messages: currentMessages,
|
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,
|
chatId: req.chatId,
|
||||||
messages: chat!.messages,
|
messages: chat!.messages,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (status.error) {
|
if (status.error) {
|
||||||
event.sender.send(
|
safeSend(
|
||||||
|
event.sender,
|
||||||
"chat:response:error",
|
"chat:response:error",
|
||||||
`Sorry, there was an error applying the AI's changes: ${status.error}`,
|
`Sorry, there was an error applying the AI's changes: ${status.error}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signal that the stream has completed
|
// Signal that the stream has completed
|
||||||
event.sender.send("chat:response:end", {
|
safeSend(event.sender, "chat:response:end", {
|
||||||
chatId: req.chatId,
|
chatId: req.chatId,
|
||||||
updatedFiles: status.updatedFiles ?? false,
|
updatedFiles: status.updatedFiles ?? false,
|
||||||
extraFiles: status.extraFiles,
|
extraFiles: status.extraFiles,
|
||||||
extraFilesError: status.extraFilesError,
|
extraFilesError: status.extraFilesError,
|
||||||
} satisfies ChatResponseEnd);
|
} satisfies ChatResponseEnd);
|
||||||
} else {
|
} else {
|
||||||
event.sender.send("chat:response:end", {
|
safeSend(event.sender, "chat:response:end", {
|
||||||
chatId: req.chatId,
|
chatId: req.chatId,
|
||||||
updatedFiles: false,
|
updatedFiles: false,
|
||||||
} satisfies ChatResponseEnd);
|
} satisfies ChatResponseEnd);
|
||||||
@@ -674,7 +677,8 @@ This conversation includes one or more image attachments. When the user uploads
|
|||||||
return req.chatId;
|
return req.chatId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error calling LLM:", error);
|
logger.error("Error calling LLM:", error);
|
||||||
event.sender.send(
|
safeSend(
|
||||||
|
event.sender,
|
||||||
"chat:response:error",
|
"chat:response:error",
|
||||||
`Sorry, there was an error processing your request: ${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
|
// Send the end event to the renderer
|
||||||
event.sender.send("chat:response:end", {
|
safeSend(event.sender, "chat:response:end", {
|
||||||
chatId,
|
chatId,
|
||||||
updatedFiles: false,
|
updatedFiles: false,
|
||||||
} satisfies ChatResponseEnd);
|
} satisfies ChatResponseEnd);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
createTestOnlyLoggedHandler,
|
createTestOnlyLoggedHandler,
|
||||||
} from "./safe_handle";
|
} from "./safe_handle";
|
||||||
import { handleSupabaseOAuthReturn } from "../../supabase_admin/supabase_return_handler";
|
import { handleSupabaseOAuthReturn } from "../../supabase_admin/supabase_return_handler";
|
||||||
|
import { safeSend } from "../utils/safe_sender";
|
||||||
|
|
||||||
const logger = log.scope("supabase_handlers");
|
const logger = log.scope("supabase_handlers");
|
||||||
const handle = createLoggedHandler(logger);
|
const handle = createLoggedHandler(logger);
|
||||||
@@ -70,7 +71,7 @@ export function registerSupabaseHandlers() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Simulate the deep link event
|
// Simulate the deep link event
|
||||||
event.sender.send("deep-link-received", {
|
safeSend(event.sender, "deep-link-received", {
|
||||||
type: "supabase-oauth-return",
|
type: "supabase-oauth-return",
|
||||||
url: "https://supabase-oauth.dyad.sh/api/connect-supabase/login",
|
url: "https://supabase-oauth.dyad.sh/api/connect-supabase/login",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { safeSend } from "../utils/safe_sender";
|
||||||
|
|
||||||
// e.g. [dyad-qa=add-dep]
|
// e.g. [dyad-qa=add-dep]
|
||||||
// Canned responses for test prompts
|
// Canned responses for test prompts
|
||||||
const TEST_RESPONSES: Record<string, string> = {
|
const TEST_RESPONSES: Record<string, string> = {
|
||||||
@@ -64,7 +66,7 @@ export async function streamTestResponse(
|
|||||||
fullResponse += chunk + " ";
|
fullResponse += chunk + " ";
|
||||||
|
|
||||||
// Send the current accumulated response
|
// Send the current accumulated response
|
||||||
event.sender.send("chat:response:chunk", {
|
safeSend(event.sender, "chat:response:chunk", {
|
||||||
chatId: chatId,
|
chatId: chatId,
|
||||||
messages: [
|
messages: [
|
||||||
...updatedChat.messages,
|
...updatedChat.messages,
|
||||||
|
|||||||
29
src/ipc/utils/safe_sender.ts
Normal file
29
src/ipc/utils/safe_sender.ts
Normal 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}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user