@bobfrankston/mailx-imap 0.1.102 → 0.1.104
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.js +61 -24
- package/package.json +3 -3
package/index.js
CHANGED
|
@@ -1331,7 +1331,8 @@ export class ImapManager extends EventEmitter {
|
|
|
1331
1331
|
* caller's deletion-reconcile can reuse it instead of paying for a second
|
|
1332
1332
|
* UID SEARCH round-trip.
|
|
1333
1333
|
*/
|
|
1334
|
-
async fetchServerOnlyUids(client, accountId, folderId,
|
|
1334
|
+
async fetchServerOnlyUids(client, accountId, folderId, folder, statusMessageCount, excludeUids) {
|
|
1335
|
+
const folderPath = folder.path;
|
|
1335
1336
|
const existingUids = this.db.getUidsForFolder(accountId, folderId);
|
|
1336
1337
|
// Small folders (Drafts, Sent, …) get a FULL UID fetch so server-side
|
|
1337
1338
|
// deletions of OLDER messages are reflected; large folders (134k INBOX)
|
|
@@ -1359,29 +1360,53 @@ export class ImapManager extends EventEmitter {
|
|
|
1359
1360
|
console.log(` [sync-uids] ${accountId}/${folderPath}: ${serverUids.length} UIDs in ${Date.now() - __t0}ms`);
|
|
1360
1361
|
const existingSet = new Set(existingUids);
|
|
1361
1362
|
const missingUids = serverUids.filter((uid) => !existingSet.has(uid) && !excludeUids.has(uid));
|
|
1362
|
-
const recovered = [];
|
|
1363
1363
|
if (missingUids.length === 0)
|
|
1364
|
-
return {
|
|
1364
|
+
return { recoveredCount: 0, serverUids, dateBounded };
|
|
1365
1365
|
// Cap so a misbehaving response / brand-new account doesn't pull tens
|
|
1366
1366
|
// of thousands at once; the rest resumes next cycle. Higher UID ≈ more
|
|
1367
|
-
// recent under stable UIDVALIDITY, so prefer newest when capping
|
|
1367
|
+
// recent under stable UIDVALIDITY, so prefer newest when capping — the
|
|
1368
|
+
// user cares most about recent mail and the deficit gate keeps calling
|
|
1369
|
+
// us until the backlog is drained.
|
|
1370
|
+
//
|
|
1371
|
+
// Kept deliberately small (Bob 2026-06-20): each backfilled batch feeds
|
|
1372
|
+
// the body-prefetcher, which issues large UID FETCHes on the single
|
|
1373
|
+
// shared slow-lane connection. A 5000-message batch flooded that
|
|
1374
|
+
// connection (cmd-chain waits of 70s+, 90s FETCH timeouts) and wedged
|
|
1375
|
+
// the whole sync worker. A small batch per cycle keeps prefetch's queue
|
|
1376
|
+
// shallow so the connection stays responsive; the deficit gate drains
|
|
1377
|
+
// the backlog over more cycles instead of one connection-killing burst.
|
|
1368
1378
|
const BACKFILL_CHUNK_SIZE = 100;
|
|
1369
|
-
const RECONCILE_FETCH_CAP =
|
|
1379
|
+
const RECONCILE_FETCH_CAP = 1000;
|
|
1370
1380
|
let toFetch = missingUids;
|
|
1371
1381
|
if (missingUids.length > RECONCILE_FETCH_CAP) {
|
|
1372
|
-
console.log(` ${folderPath}: ${missingUids.length} server-only UIDs — capped at ${RECONCILE_FETCH_CAP};
|
|
1382
|
+
console.log(` ${folderPath}: ${missingUids.length} server-only UIDs — capped at ${RECONCILE_FETCH_CAP}; resumes next cycle`);
|
|
1373
1383
|
toFetch = missingUids.slice().sort((a, b) => b - a).slice(0, RECONCILE_FETCH_CAP);
|
|
1374
1384
|
}
|
|
1375
1385
|
else {
|
|
1376
1386
|
console.log(` ${folderPath}: ${missingUids.length} server-only UIDs — fetching`);
|
|
1377
1387
|
}
|
|
1388
|
+
// Store each chunk as it arrives so a mid-stream connection drop (the
|
|
1389
|
+
// "Not connected" / desync class on this server) keeps the progress it
|
|
1390
|
+
// already made instead of losing the whole batch. Stop on the first
|
|
1391
|
+
// chunk error; the deficit gate calls us again next cycle to resume.
|
|
1392
|
+
let recoveredCount = 0;
|
|
1378
1393
|
for (let i = 0; i < toFetch.length; i += BACKFILL_CHUNK_SIZE) {
|
|
1379
1394
|
const chunk = toFetch.slice(i, i + BACKFILL_CHUNK_SIZE);
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1395
|
+
try {
|
|
1396
|
+
const got = await client.fetchMessages(folderPath, chunk.join(","), { source: false });
|
|
1397
|
+
recoveredCount += await this.storeMessages(accountId, folderId, folder, got, 0);
|
|
1398
|
+
}
|
|
1399
|
+
catch (e) {
|
|
1400
|
+
console.error(` ${folderPath}: backfill chunk ${i}-${i + chunk.length} failed (${e?.message || e}) — keeping ${recoveredCount} so far, resuming next cycle`);
|
|
1401
|
+
break;
|
|
1402
|
+
}
|
|
1403
|
+
console.log(` ${folderPath}: backfill ${Math.min(i + BACKFILL_CHUNK_SIZE, toFetch.length)}/${toFetch.length} (stored ${recoveredCount})`);
|
|
1383
1404
|
}
|
|
1384
|
-
|
|
1405
|
+
if (recoveredCount > 0) {
|
|
1406
|
+
this.db.recalcFolderCounts(folderId);
|
|
1407
|
+
this.emit("folderCountsChanged", accountId, {});
|
|
1408
|
+
}
|
|
1409
|
+
return { recoveredCount, serverUids, dateBounded };
|
|
1385
1410
|
}
|
|
1386
1411
|
async syncFolder(accountId, folderId, client) {
|
|
1387
1412
|
const lockKey = `${accountId}:${folderId}`;
|
|
@@ -1590,17 +1615,28 @@ export class ImapManager extends EventEmitter {
|
|
|
1590
1615
|
// set-diff on the first sync after boot, then at most once
|
|
1591
1616
|
// per RECONCILE_THROTTLE_MS so the fast path stays fast.
|
|
1592
1617
|
let backfilledCount = 0;
|
|
1618
|
+
// Run the set-diff when EITHER the folder has a known
|
|
1619
|
+
// deficit (local count < server count → messages are
|
|
1620
|
+
// missing, keep reconciling every cycle until caught up) OR
|
|
1621
|
+
// the throttle window elapsed (periodic safety sweep for
|
|
1622
|
+
// gaps that don't move the count, e.g. a desync-dropped
|
|
1623
|
+
// message replaced by a later arrival). On success advance
|
|
1624
|
+
// the throttle; on FAILURE leave it so the next cycle
|
|
1625
|
+
// retries instead of locking out for RECONCILE_THROTTLE_MS
|
|
1626
|
+
// (Bob 2026-06-20: boot-time "Not connected" failure locked
|
|
1627
|
+
// INBOX recovery out for 15 min).
|
|
1628
|
+
const localCount = this.db.getMessageCount(accountId, folderId);
|
|
1629
|
+
const hasDeficit = statusMessageCount !== null && localCount < statusMessageCount;
|
|
1593
1630
|
const lastRecon = this.lastReconcileMs.get(folderId) ?? 0;
|
|
1594
|
-
|
|
1595
|
-
|
|
1631
|
+
const throttleElapsed = Date.now() - lastRecon > RECONCILE_THROTTLE_MS;
|
|
1632
|
+
if (hasDeficit || throttleElapsed) {
|
|
1596
1633
|
try {
|
|
1597
|
-
const sd = await this.fetchServerOnlyUids(client, accountId, folderId, folder
|
|
1598
|
-
|
|
1599
|
-
|
|
1634
|
+
const sd = await this.fetchServerOnlyUids(client, accountId, folderId, folder, statusMessageCount, new Set(newOnes.map((m) => m.uid)));
|
|
1635
|
+
backfilledCount = sd.recoveredCount;
|
|
1636
|
+
if (backfilledCount > 0) {
|
|
1600
1637
|
console.log(` [qresync] ${accountId}/${folder.path}: set-diff backfilled ${backfilledCount} server-only UID(s)`);
|
|
1601
|
-
this.db.recalcFolderCounts(folderId);
|
|
1602
|
-
this.emit("folderCountsChanged", accountId, {});
|
|
1603
1638
|
}
|
|
1639
|
+
this.lastReconcileMs.set(folderId, Date.now());
|
|
1604
1640
|
}
|
|
1605
1641
|
catch (e) {
|
|
1606
1642
|
console.error(` [qresync] ${accountId}/${folder.path}: set-diff backfill failed: ${e?.message || e}`);
|
|
@@ -1677,21 +1713,22 @@ export class ImapManager extends EventEmitter {
|
|
|
1677
1713
|
// historical messages in one cycle. Hit the cap and the next
|
|
1678
1714
|
// sync picks up the rest.
|
|
1679
1715
|
try {
|
|
1680
|
-
// Shared with the QRESYNC fast path (fetchServerOnlyUids).
|
|
1681
|
-
//
|
|
1682
|
-
//
|
|
1683
|
-
//
|
|
1684
|
-
|
|
1685
|
-
|
|
1716
|
+
// Shared with the QRESYNC fast path (fetchServerOnlyUids). It
|
|
1717
|
+
// stores recovered messages itself (chunk-at-a-time, so a
|
|
1718
|
+
// dropped connection keeps partial progress). Exclude the UIDs
|
|
1719
|
+
// we just fetched via fetchMessagesSinceUid — they're in
|
|
1720
|
+
// `messages`, about to be stored by the batch loop below, so
|
|
1721
|
+
// they'd otherwise look "missing" and get re-fetched.
|
|
1722
|
+
const sd = await this.fetchServerOnlyUids(client, accountId, folderId, folder, statusMessageCount, new Set(messages.map(m => m.uid)));
|
|
1686
1723
|
// Stash the server UID list for the deletion-reconciliation
|
|
1687
1724
|
// block below — no point hitting the server a second time.
|
|
1688
1725
|
serverUidsCached = sd.serverUids;
|
|
1689
1726
|
serverUidsAreDateBounded = sd.dateBounded;
|
|
1727
|
+
this.lastReconcileMs.set(folderId, Date.now());
|
|
1690
1728
|
}
|
|
1691
1729
|
catch (e) {
|
|
1692
1730
|
console.error(` ${folder.path}: reconciliation failed: ${e.message}`);
|
|
1693
1731
|
}
|
|
1694
|
-
this.lastReconcileMs.set(folderId, Date.now());
|
|
1695
1732
|
// Date-based backfill via SEARCH SINCE was here. Removed —
|
|
1696
1733
|
// set-diff above already covers the same case (any server UID
|
|
1697
1734
|
// not in our local set gets fetched, regardless of whether it's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.104",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/mailx-types": "^0.1.19",
|
|
13
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
13
|
+
"@bobfrankston/mailx-settings": "^0.1.30",
|
|
14
14
|
"@bobfrankston/mailx-store": "^0.1.54",
|
|
15
15
|
"@bobfrankston/iflow-direct": "^0.1.55",
|
|
16
16
|
"@bobfrankston/tcp-transport": "^0.1.7",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
".transformedSnapshot": {
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bobfrankston/mailx-types": "^0.1.19",
|
|
41
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
41
|
+
"@bobfrankston/mailx-settings": "^0.1.30",
|
|
42
42
|
"@bobfrankston/mailx-store": "^0.1.54",
|
|
43
43
|
"@bobfrankston/iflow-direct": "^0.1.55",
|
|
44
44
|
"@bobfrankston/tcp-transport": "^0.1.7",
|