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 <userData>/main-errors.log instead of raising the modal;
Electron continued after that dialog anyway, so only the interruption
goes.
This commit is contained in:
Local Dev 2026-09-27 10:15:22 +02:00
parent d3787f8a29
commit 729930f0b5

29
main.js
View file

@ -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;