@bobfrankston/rmfmail 1.1.186 → 1.1.188

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.
@@ -223,6 +223,14 @@ async function withTimeout<T>(promise: Promise<T>, ms: number, client: any, labe
223
223
  export class ImapManager extends EventEmitter {
224
224
  private configs: Map<string, ReturnType<typeof createAutoImapConfig>> = new Map();
225
225
  private watchers: Map<string, () => void> = new Map();
226
+ // Parallel map of the IDLE watch client per account, so runFullSync /
227
+ // startWatching can health-check the live socket and refresh ONLY dead
228
+ // watchers instead of tearing every watcher down each cycle. The blanket
229
+ // teardown was the IDLE-churn driver: it dropped healthy IDLE every 5 min,
230
+ // and re-establishing competed for the saturated 4-permit host semaphore
231
+ // (folder syncs holding all permits, timing out at 360s), leaving new mail
232
+ // on 5-min polling for minutes at a time (Bob 2026-05-28).
233
+ private watcherClients: Map<string, any> = new Map();
226
234
  private fetchClients: Map<string, any> = new Map();
227
235
  /** The Store is the architectural nexus — owner of MailxDB +
228
236
  * FileMessageStore + the event bus. This package (mailx-imap) is a
@@ -357,7 +365,7 @@ export class ImapManager extends EventEmitter {
357
365
 
358
366
  // Stop IDLE watcher for this account
359
367
  const stopWatcher = this.watchers.get(accountId);
360
- if (stopWatcher) { try { await stopWatcher(); } catch { /* */ } this.watchers.delete(accountId); }
368
+ if (stopWatcher) { try { await stopWatcher(); } catch { /* */ } this.watchers.delete(accountId); this.watcherClients.delete(accountId); }
361
369
 
362
370
  // Delete only the IMAP token (not contacts — separate scope, separate consent)
363
371
  const accountDir = tokenDirName(account.imap.user);
@@ -2513,7 +2521,12 @@ export class ImapManager extends EventEmitter {
2513
2521
  async runFullSync(): Promise<void> {
2514
2522
  console.log(` [periodic] Full sync at ${new Date().toLocaleTimeString()}`);
2515
2523
  await this.syncAll();
2516
- await this.stopWatching();
2524
+ // Do NOT tear down healthy IDLE here. startWatching is idempotent and
2525
+ // now health-checks each existing watcher: alive ones are left intact,
2526
+ // dead/missing ones are re-established. The old unconditional
2527
+ // stopWatching()+startWatching() dropped healthy IDLE every cycle and
2528
+ // the re-establish stalled behind the saturated host semaphore — the
2529
+ // "still slow polling / not subscribing" gaps (Bob 2026-05-28).
2517
2530
  await this.startWatching();
2518
2531
  }
2519
2532
 
@@ -2537,7 +2550,22 @@ export class ImapManager extends EventEmitter {
2537
2550
  * right way to make that cheap, not piling per-folder IDLE sockets). */
2538
2551
  async startWatching(): Promise<void> {
2539
2552
  for (const [accountId] of this.configs) {
2540
- if (this.watchers.has(accountId)) continue;
2553
+ if (this.watchers.has(accountId)) {
2554
+ // Already watching — but is the socket still alive? A silently
2555
+ // dropped IDLE leaves the watcher entry in place, so without
2556
+ // this health-check the deadman (which only checks for MISSING
2557
+ // entries) would never refresh it. If dead, tear down just THIS
2558
+ // one and fall through to re-establish; if alive, leave it.
2559
+ const wc = this.watcherClients.get(accountId);
2560
+ const sock = wc?.native?.transport?.socket;
2561
+ const dead = !wc || sock?.destroyed || sock?.readyState === "closed" || wc?._dead;
2562
+ if (!dead) continue;
2563
+ console.log(` [idle] ${accountId}: watcher socket dead — refreshing`);
2564
+ const stop = this.watchers.get(accountId);
2565
+ if (stop) { try { await stop(); } catch { /* */ } }
2566
+ this.watchers.delete(accountId);
2567
+ this.watcherClients.delete(accountId);
2568
+ }
2541
2569
  try {
2542
2570
  // IDLE keeps its own dedicated socket — once the connection
2543
2571
  // is parked in IDLE, it's unusable for any other command, so
@@ -2602,6 +2630,7 @@ export class ImapManager extends EventEmitter {
2602
2630
  await stop();
2603
2631
  await watchClient.logout();
2604
2632
  });
2633
+ this.watcherClients.set(accountId, watchClient);
2605
2634
  console.log(` [idle] Watching INBOX for ${accountId}${useNotify ? " (+NOTIFY personal mailboxes)" : ""}`);
2606
2635
  } catch (e: any) {
2607
2636
  // Defensive: same empty-message bug class as the transport
@@ -2628,6 +2657,7 @@ export class ImapManager extends EventEmitter {
2628
2657
  try { await stop(); } catch { /* ignore */ }
2629
2658
  }
2630
2659
  this.watchers.clear();
2660
+ this.watcherClients.clear();
2631
2661
  }
2632
2662
 
2633
2663
  /** Unlink the on-disk body file for a message by reading its `body_path`
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-imap",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bobfrankston/mailx-imap",
9
- "version": "0.1.70",
9
+ "version": "0.1.71",
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@bobfrankston/iflow-direct": "^0.1.27",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-imap",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",