@bobfrankston/mailx-imap 0.1.70 → 0.1.71
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/index.d.ts +1 -0
- package/index.js +39 -3
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -59,6 +59,7 @@ export interface OutboxStatus {
|
|
|
59
59
|
export declare class ImapManager extends EventEmitter {
|
|
60
60
|
private configs;
|
|
61
61
|
private watchers;
|
|
62
|
+
private watcherClients;
|
|
62
63
|
private fetchClients;
|
|
63
64
|
/** The Store is the architectural nexus — owner of MailxDB +
|
|
64
65
|
* FileMessageStore + the event bus. This package (mailx-imap) is a
|
package/index.js
CHANGED
|
@@ -158,6 +158,14 @@ async function withTimeout(promise, ms, client, label) {
|
|
|
158
158
|
export class ImapManager extends EventEmitter {
|
|
159
159
|
configs = new Map();
|
|
160
160
|
watchers = new Map();
|
|
161
|
+
// Parallel map of the IDLE watch client per account, so runFullSync /
|
|
162
|
+
// startWatching can health-check the live socket and refresh ONLY dead
|
|
163
|
+
// watchers instead of tearing every watcher down each cycle. The blanket
|
|
164
|
+
// teardown was the IDLE-churn driver: it dropped healthy IDLE every 5 min,
|
|
165
|
+
// and re-establishing competed for the saturated 4-permit host semaphore
|
|
166
|
+
// (folder syncs holding all permits, timing out at 360s), leaving new mail
|
|
167
|
+
// on 5-min polling for minutes at a time (Bob 2026-05-28).
|
|
168
|
+
watcherClients = new Map();
|
|
161
169
|
fetchClients = new Map();
|
|
162
170
|
/** The Store is the architectural nexus — owner of MailxDB +
|
|
163
171
|
* FileMessageStore + the event bus. This package (mailx-imap) is a
|
|
@@ -294,6 +302,7 @@ export class ImapManager extends EventEmitter {
|
|
|
294
302
|
}
|
|
295
303
|
catch { /* */ }
|
|
296
304
|
this.watchers.delete(accountId);
|
|
305
|
+
this.watcherClients.delete(accountId);
|
|
297
306
|
}
|
|
298
307
|
// Delete only the IMAP token (not contacts — separate scope, separate consent)
|
|
299
308
|
const accountDir = tokenDirName(account.imap.user);
|
|
@@ -2473,7 +2482,12 @@ export class ImapManager extends EventEmitter {
|
|
|
2473
2482
|
async runFullSync() {
|
|
2474
2483
|
console.log(` [periodic] Full sync at ${new Date().toLocaleTimeString()}`);
|
|
2475
2484
|
await this.syncAll();
|
|
2476
|
-
|
|
2485
|
+
// Do NOT tear down healthy IDLE here. startWatching is idempotent and
|
|
2486
|
+
// now health-checks each existing watcher: alive ones are left intact,
|
|
2487
|
+
// dead/missing ones are re-established. The old unconditional
|
|
2488
|
+
// stopWatching()+startWatching() dropped healthy IDLE every cycle and
|
|
2489
|
+
// the re-establish stalled behind the saturated host semaphore — the
|
|
2490
|
+
// "still slow polling / not subscribing" gaps (Bob 2026-05-28).
|
|
2477
2491
|
await this.startWatching();
|
|
2478
2492
|
}
|
|
2479
2493
|
/** Stop periodic sync */
|
|
@@ -2494,8 +2508,28 @@ export class ImapManager extends EventEmitter {
|
|
|
2494
2508
|
* right way to make that cheap, not piling per-folder IDLE sockets). */
|
|
2495
2509
|
async startWatching() {
|
|
2496
2510
|
for (const [accountId] of this.configs) {
|
|
2497
|
-
if (this.watchers.has(accountId))
|
|
2498
|
-
|
|
2511
|
+
if (this.watchers.has(accountId)) {
|
|
2512
|
+
// Already watching — but is the socket still alive? A silently
|
|
2513
|
+
// dropped IDLE leaves the watcher entry in place, so without
|
|
2514
|
+
// this health-check the deadman (which only checks for MISSING
|
|
2515
|
+
// entries) would never refresh it. If dead, tear down just THIS
|
|
2516
|
+
// one and fall through to re-establish; if alive, leave it.
|
|
2517
|
+
const wc = this.watcherClients.get(accountId);
|
|
2518
|
+
const sock = wc?.native?.transport?.socket;
|
|
2519
|
+
const dead = !wc || sock?.destroyed || sock?.readyState === "closed" || wc?._dead;
|
|
2520
|
+
if (!dead)
|
|
2521
|
+
continue;
|
|
2522
|
+
console.log(` [idle] ${accountId}: watcher socket dead — refreshing`);
|
|
2523
|
+
const stop = this.watchers.get(accountId);
|
|
2524
|
+
if (stop) {
|
|
2525
|
+
try {
|
|
2526
|
+
await stop();
|
|
2527
|
+
}
|
|
2528
|
+
catch { /* */ }
|
|
2529
|
+
}
|
|
2530
|
+
this.watchers.delete(accountId);
|
|
2531
|
+
this.watcherClients.delete(accountId);
|
|
2532
|
+
}
|
|
2499
2533
|
try {
|
|
2500
2534
|
// IDLE keeps its own dedicated socket — once the connection
|
|
2501
2535
|
// is parked in IDLE, it's unusable for any other command, so
|
|
@@ -2557,6 +2591,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2557
2591
|
await stop();
|
|
2558
2592
|
await watchClient.logout();
|
|
2559
2593
|
});
|
|
2594
|
+
this.watcherClients.set(accountId, watchClient);
|
|
2560
2595
|
console.log(` [idle] Watching INBOX for ${accountId}${useNotify ? " (+NOTIFY personal mailboxes)" : ""}`);
|
|
2561
2596
|
}
|
|
2562
2597
|
catch (e) {
|
|
@@ -2590,6 +2625,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2590
2625
|
catch { /* ignore */ }
|
|
2591
2626
|
}
|
|
2592
2627
|
this.watchers.clear();
|
|
2628
|
+
this.watcherClients.clear();
|
|
2593
2629
|
}
|
|
2594
2630
|
/** Unlink the on-disk body file for a message by reading its `body_path`
|
|
2595
2631
|
* from the DB. Safe to call either before or after `db.deleteMessage`
|