From 729930f0b524e8e03cc8dde4ba6f782d5efad320 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 27 Sep 2026 10:15:22 +0200 Subject: [PATCH] fix(theseus): closing a still-connecting relay socket must not crash the main process Node's ws aborts the handshake when close() is called on a CONNECTING socket and emits an error on the next tick; with no listener that is an uncaught exception, and Electron answers with the modal "A JavaScript error occurred in the main process". nostr-tools drops its onerror handler right before closing, which is what the WizardConnect relay teardown in Aegis runs on every wallet disconnect while a relay is still connecting. Browser WebSockets ignore the same sequence, which is why the library gets away with it elsewhere. Every consumer in the main process shares the one ws module, so close() now adds a no-op error listener to a connecting socket before aborting it. Anything else that still escapes to the top of the process is logged to /main-errors.log instead of raising the modal; Electron continued after that dialog anyway, so only the interruption goes. --- main.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/main.js b/main.js index 16327985..00f2b0fd 100644 --- a/main.js +++ b/main.js @@ -23,6 +23,35 @@ const webapps = require("./webapps"); // them, so a write that cannot land is not an error worth stopping for. for (const stream of [process.stdout, process.stderr]) stream.on("error", () => {}); +// Node's ws turns "closed while still connecting" into a crash: close() on a +// CONNECTING socket aborts the handshake and emits an error on the next tick, +// and an error event with no listener is an uncaught exception. Browser +// WebSockets shrug the same thing off, so libraries written for both do it — +// nostr-tools drops its onerror handler and then closes the socket, which is +// what the WizardConnect relay teardown in Aegis runs on every wallet +// disconnect. Seen 2026-09-27 as the "JavaScript error occurred in the main +// process" dialog. Every consumer in this process (messenger, add-ons, +// WizardConnect via isomorphic-ws) shares this one ws module, so guard it here. +{ + const close = WebSocket.prototype.close; + WebSocket.prototype.close = function (...args) { + if (this.readyState === WebSocket.CONNECTING && this.listenerCount("error") === 0) this.once("error", () => {}); + return close.apply(this, args); + }; +} +// Anything else that escapes to the top of the main process: Electron would +// show a modal "JavaScript error occurred in the main process" and carry on. +// Carry on without the modal, and keep the report where it can be found. +process.on("uncaughtException", (err) => { + const line = `[${new Date().toISOString()}] uncaught: ${(err && err.stack) || err}\n`; + try { console.error(line); } catch {} + try { + const f = path.join(app.getPath("userData"), "main-errors.log"); + try { if (fs.statSync(f).size > 512 * 1024) fs.truncateSync(f, 0); } catch {} + fs.appendFileSync(f, line); + } catch {} +}); + // Packaged builds ship the resolver and tor/ as unpacked resources (they can't // run from inside app.asar); dev runs read them from the repo. const RES_DIR = app.isPackaged ? process.resourcesPath : __dirname;