@bobfrankston/mailx 1.0.134 → 1.0.135
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.135",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"postinstall": "node launcher/builder/postinstall.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@bobfrankston/iflow": "^1.0.
|
|
23
|
+
"@bobfrankston/iflow": "^1.0.51",
|
|
24
24
|
"@bobfrankston/miscinfo": "^1.0.7",
|
|
25
25
|
"@bobfrankston/oauthsupport": "^1.0.20",
|
|
26
26
|
"@bobfrankston/rust-builder": "^0.1.3",
|
|
@@ -234,10 +234,41 @@ async function start() {
|
|
|
234
234
|
// Start HTTP server FIRST so UI is always reachable (even during IMAP startup)
|
|
235
235
|
const externalAccess = process.argv.includes("--external");
|
|
236
236
|
const hostname = externalAccess ? "0.0.0.0" : "127.0.0.1";
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
// Retry listen with backoff — Windows CLOSE_WAIT zombies can hold the port for minutes after a crash
|
|
238
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
239
|
+
server = createServer(app);
|
|
240
|
+
// Prevent CLOSE_WAIT accumulation: short keepAlive timeout + connection close headers
|
|
241
|
+
server.keepAliveTimeout = 5000; // close idle keep-alive connections after 5s
|
|
242
|
+
server.headersTimeout = 10000; // kill connections with no headers after 10s
|
|
243
|
+
// Track connections for clean shutdown (prevents CLOSE_WAIT zombies on Windows)
|
|
244
|
+
server.on("connection", (conn) => {
|
|
245
|
+
openConnections.add(conn);
|
|
246
|
+
conn.on("close", () => openConnections.delete(conn));
|
|
247
|
+
});
|
|
248
|
+
// Suppress EADDRINUSE from bubbling to uncaughtException — we handle it here
|
|
249
|
+
server.on("error", () => { }); // will be replaced by listen handler below
|
|
250
|
+
wss = new WebSocketServer({ server });
|
|
251
|
+
wireWebSocket();
|
|
252
|
+
const listenResult = await new Promise((resolve) => {
|
|
253
|
+
server.removeAllListeners("error");
|
|
254
|
+
server.once("error", (e) => { resolve(e.code || e.message); });
|
|
255
|
+
server.listen({ port: PORT, host: hostname, exclusive: false }, () => {
|
|
256
|
+
server.removeAllListeners("error");
|
|
257
|
+
resolve("ok");
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
if (listenResult === "ok")
|
|
261
|
+
break;
|
|
262
|
+
if (listenResult === "EADDRINUSE" && attempt < 29) {
|
|
263
|
+
const wait = Math.min(2000 + attempt * 1000, 10000);
|
|
264
|
+
console.log(` Port ${PORT} in use (CLOSE_WAIT zombies?) — retry ${attempt + 1}/30 in ${wait / 1000}s...`);
|
|
265
|
+
server.close();
|
|
266
|
+
await new Promise(r => setTimeout(r, wait));
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
throw new Error(`Cannot bind port ${PORT}: ${listenResult}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
241
272
|
console.log(`mailx server running on http://${hostname}:${PORT}`);
|
|
242
273
|
// Seed contacts (fast — skips existing)
|
|
243
274
|
const seeded = db.seedContactsFromMessages();
|
|
@@ -287,6 +318,8 @@ async function start() {
|
|
|
287
318
|
imapManager.startOutboxWorker();
|
|
288
319
|
}
|
|
289
320
|
// ── Graceful Shutdown ──
|
|
321
|
+
/** Track all open connections so we can destroy them on shutdown (prevents CLOSE_WAIT zombies) */
|
|
322
|
+
const openConnections = new Set();
|
|
290
323
|
async function shutdown() {
|
|
291
324
|
console.log("\nShutting down...");
|
|
292
325
|
const forceExit = setTimeout(() => { console.log("Force exit"); process.exit(1); }, 3000);
|
|
@@ -296,6 +329,11 @@ async function shutdown() {
|
|
|
296
329
|
}
|
|
297
330
|
catch { /* proceed */ }
|
|
298
331
|
db.close();
|
|
332
|
+
// Destroy all open connections immediately — prevents CLOSE_WAIT zombies on Windows
|
|
333
|
+
for (const conn of openConnections) {
|
|
334
|
+
conn.destroy();
|
|
335
|
+
}
|
|
336
|
+
openConnections.clear();
|
|
299
337
|
server?.close();
|
|
300
338
|
clearTimeout(forceExit);
|
|
301
339
|
process.exit(0);
|
|
@@ -308,11 +346,6 @@ process.on("unhandledRejection", (err) => {
|
|
|
308
346
|
process.on("uncaughtException", (err) => {
|
|
309
347
|
console.error("FATAL uncaught exception:", err.message);
|
|
310
348
|
console.error(err.stack);
|
|
311
|
-
// EADDRINUSE = another instance holds the port — exit so node --watch can retry
|
|
312
|
-
if (err.code === "EADDRINUSE") {
|
|
313
|
-
console.error("Port in use — exiting so node --watch can retry");
|
|
314
|
-
process.exit(1);
|
|
315
|
-
}
|
|
316
349
|
// Other exceptions: stay alive, let node --watch handle file-change restarts
|
|
317
350
|
});
|
|
318
351
|
process.on("exit", (code) => {
|