@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.134",
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.50",
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
- server = createServer(app);
238
- wss = new WebSocketServer({ server });
239
- wireWebSocket();
240
- await new Promise((resolve) => server.listen(PORT, hostname, resolve));
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) => {