@bobfrankston/rmfmail 1.0.589 → 1.0.592
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/client/components/folder-tree.js +32 -3
- package/client/components/folder-tree.js.map +1 -1
- package/client/components/folder-tree.ts +32 -3
- package/package.json +7 -7
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +41 -49
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +41 -55
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
|
@@ -1159,21 +1159,13 @@ export class ImapManager extends EventEmitter {
|
|
|
1159
1159
|
const missingUids = allServerUids.filter((uid: number) =>
|
|
1160
1160
|
!existingSet.has(uid) && !newSet.has(uid)
|
|
1161
1161
|
);
|
|
1162
|
-
// Backfill chunk size
|
|
1163
|
-
// a
|
|
1164
|
-
//
|
|
1165
|
-
//
|
|
1166
|
-
//
|
|
1167
|
-
// The 500-chunk version held the ops queue for the
|
|
1168
|
-
// entire backfill — Bob 2026-05-08 saw a click-to-render
|
|
1169
|
-
// wait of 100+ minutes on a busy backfill of the IP
|
|
1170
|
-
// folder.
|
|
1162
|
+
// Backfill chunk size. Use the passed-in `client` directly
|
|
1163
|
+
// (NOT a nested withConnection) — syncFolder is now wrapped
|
|
1164
|
+
// in withConnection at the call site, so the slow lane is
|
|
1165
|
+
// already locked for our duration. A nested withConnection
|
|
1166
|
+
// would deadlock waiting for the slot we hold.
|
|
1171
1167
|
const BACKFILL_CHUNK_SIZE = 100;
|
|
1172
1168
|
if (missingUids.length > 0 && missingUids.length <= 5000) {
|
|
1173
|
-
// For the log line we report a count; computing
|
|
1174
|
-
// min/max via a spread (`Math.min(...arr)`) blows V8's
|
|
1175
|
-
// argument limit on folders with tens of thousands of
|
|
1176
|
-
// UIDs. Use a manual reduce.
|
|
1177
1169
|
let minU = existingUids[0] ?? 0;
|
|
1178
1170
|
for (let i = 1; i < existingUids.length; i++) if (existingUids[i] < minU) minU = existingUids[i];
|
|
1179
1171
|
console.log(` ${folder.path}: ${missingUids.length} server-only UIDs (local lowest=${minU}, highest=${highestUid}) — fetching`);
|
|
@@ -1181,40 +1173,22 @@ export class ImapManager extends EventEmitter {
|
|
|
1181
1173
|
for (let i = 0; i < missingUids.length; i += BACKFILL_CHUNK_SIZE) {
|
|
1182
1174
|
const chunk = missingUids.slice(i, i + BACKFILL_CHUNK_SIZE);
|
|
1183
1175
|
const range = chunk.join(",");
|
|
1184
|
-
|
|
1185
|
-
// turn so any fast-lane click queued in the
|
|
1186
|
-
// meantime gets serviced between chunks. The
|
|
1187
|
-
// outer `client` param is bypassed here; the
|
|
1188
|
-
// queue-managed client is the same persistent
|
|
1189
|
-
// ops client (getOpsClient).
|
|
1190
|
-
const recovered = await this.withConnection(accountId, async (c) =>
|
|
1191
|
-
await (c as any).fetchMessages(folder.path, range, { source: false }),
|
|
1192
|
-
{ slow: true },
|
|
1193
|
-
);
|
|
1176
|
+
const recovered = await (client as any).fetchMessages(folder.path, range, { source: false });
|
|
1194
1177
|
messages.push(...recovered);
|
|
1195
1178
|
recoveredTotal += recovered.length;
|
|
1196
1179
|
console.log(` ${folder.path}: fetch ${recoveredTotal}/${missingUids.length}`);
|
|
1197
1180
|
}
|
|
1198
1181
|
} else if (missingUids.length > 5000) {
|
|
1199
1182
|
console.log(` ${folder.path}: ${missingUids.length} server-only UIDs — capped; will resume next cycle`);
|
|
1200
|
-
//
|
|
1201
|
-
//
|
|
1202
|
-
//
|
|
1203
|
-
//
|
|
1204
|
-
// through syncAccountViaApi and doesn't reach here —
|
|
1205
|
-
// its synthesized hash-UIDs have no temporal meaning).
|
|
1206
|
-
// If we ever wire this for a non-monotonic UID source,
|
|
1207
|
-
// sort by date instead — but that means an extra
|
|
1208
|
-
// fetch-of-INTERNALDATE round-trip, which we avoid for
|
|
1209
|
-
// free here under the IMAP guarantee.
|
|
1183
|
+
// Vanilla IMAP under stable UIDVALIDITY: higher UID =
|
|
1184
|
+
// later assignment ≈ more recent message (Dovecot/
|
|
1185
|
+
// Cyrus). Gmail-API path is separate (no temporal
|
|
1186
|
+
// meaning to its hash-UIDs).
|
|
1210
1187
|
const cappedSlice = missingUids.sort((a: number, b: number) => b - a).slice(0, 5000);
|
|
1211
1188
|
let recoveredTotal = 0;
|
|
1212
1189
|
for (let i = 0; i < cappedSlice.length; i += BACKFILL_CHUNK_SIZE) {
|
|
1213
1190
|
const chunk = cappedSlice.slice(i, i + BACKFILL_CHUNK_SIZE);
|
|
1214
|
-
const recovered = await
|
|
1215
|
-
await (c as any).fetchMessages(folder.path, chunk.join(","), { source: false }),
|
|
1216
|
-
{ slow: true },
|
|
1217
|
-
);
|
|
1191
|
+
const recovered = await (client as any).fetchMessages(folder.path, chunk.join(","), { source: false });
|
|
1218
1192
|
messages.push(...recovered);
|
|
1219
1193
|
recoveredTotal += recovered.length;
|
|
1220
1194
|
console.log(` ${folder.path}: fetch ${recoveredTotal}/5000 (capped)`);
|
|
@@ -1610,26 +1584,38 @@ export class ImapManager extends EventEmitter {
|
|
|
1610
1584
|
const isTrashChild = folder.path.includes("/") && folder.path.toLowerCase().startsWith("trash");
|
|
1611
1585
|
const highestUid = this.db.getHighestUid(accountId, folder.id);
|
|
1612
1586
|
if (isTrashChild && highestUid === 0) return;
|
|
1613
|
-
let
|
|
1587
|
+
let clientForDiag: any = null;
|
|
1614
1588
|
try {
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
]);
|
|
1589
|
+
// Route syncFolder through the slow-lane queue so prefetch
|
|
1590
|
+
// (also slow lane) and sync take strict turns on the slow
|
|
1591
|
+
// client. Previously syncOne grabbed `getOpsClient` and
|
|
1592
|
+
// ran syncFolder directly OUTSIDE the queue; prefetch
|
|
1593
|
+
// chunks via withConnection raced against it on the same
|
|
1594
|
+
// client. Symptom: prefetch sent `SELECT INBOX` then sync
|
|
1595
|
+
// sent `SELECT Sent/Drafts`, then prefetch's `UID FETCH
|
|
1596
|
+
// <inbox-uids>` ran against Drafts → 0 bodies returned →
|
|
1597
|
+
// prefetch logs "0/N — NOT pruning" but bodies never
|
|
1598
|
+
// download. With C123 the fast lane has its own
|
|
1599
|
+
// independent client, so wrapping sync in slow-lane
|
|
1600
|
+
// withConnection doesn't block click-time body fetches.
|
|
1601
|
+
await this.withConnection(accountId, async (client) => {
|
|
1602
|
+
clientForDiag = client;
|
|
1603
|
+
await this.syncFolder(accountId, folder.id, client);
|
|
1604
|
+
}, { slow: true, timeoutMs: PER_FOLDER_TIMEOUT_MS });
|
|
1632
1605
|
} catch (e: any) {
|
|
1606
|
+
// C120: per-folder timeout error appends transport
|
|
1607
|
+
// diagnostics so the [sync] log distinguishes "server
|
|
1608
|
+
// stopped responding" (sinceLastRead high) from "we
|
|
1609
|
+
// never finished writing" (writes climbing without
|
|
1610
|
+
// reads). The withConnection timeout already includes
|
|
1611
|
+
// its own message; we annotate further only for the
|
|
1612
|
+
// timeout path.
|
|
1613
|
+
if (/timeout/i.test(e?.message || "")) {
|
|
1614
|
+
const d = (clientForDiag as any)?.transport?.diagnostics;
|
|
1615
|
+
if (d) {
|
|
1616
|
+
e.message = `${e.message} [conn#${d.connId} r=${d.bytesRead}B w=${d.bytesWritten}B writes=${d.writeCount} sinceLastRead=${d.lastReadAt ? Date.now() - d.lastReadAt : -1}ms] folder=${folder.path}`;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1633
1619
|
if (e.responseText?.includes("doesn't exist")) {
|
|
1634
1620
|
this.db.deleteFolder(folder.id);
|
|
1635
1621
|
} else {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bobfrankston/mailx-imap",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.30",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/iflow-direct": "^0.1.27",
|