@bobfrankston/rmfmail 1.1.102 → 1.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.
@@ -1043,31 +1043,72 @@ export class MailxService {
1043
1043
  const seen = new Set();
1044
1044
  const items = [];
1045
1045
  let total = 0;
1046
+ // Partial-failure accounting. The old code did `if (r.status !==
1047
+ // "fulfilled") continue` — a folder whose SEARCH rejected
1048
+ // (connection discard, a genuinely slow huge folder hitting the
1049
+ // 90s cap) contributed zero results AND zero signal. The user saw
1050
+ // "No results" for a search that simply never ran in the folder
1051
+ // holding the match. Now we count failures and hand them back so
1052
+ // the UI can say "searched 88/93 — 5 folders failed, retry".
1053
+ let foldersSearched = 0;
1054
+ let foldersFailed = 0;
1055
+ const failedFolders = [];
1056
+ let droppedHits = 0;
1057
+ // Bounded concurrency. The per-account ops queue already
1058
+ // serializes connection use, so this doesn't change IMAP
1059
+ // parallelism — but firing all ~93 folder promises at once
1060
+ // builds a 90+-deep pending-promise queue with 90+ live
1061
+ // setTimeout timers. Batching keeps that bounded and gives a
1062
+ // natural place to add early-abort later.
1063
+ const SERVER_SEARCH_BATCH = 8;
1046
1064
  for (const acct of dbAccounts) {
1047
1065
  const folders = this.db.getFolders(acct.id)
1048
1066
  .filter((f) => !(f.flags || []).some((x) => /noselect/i.test(x)));
1049
- const results = await Promise.allSettled(folders.map(f => this.imapManager.searchAndFetchOnServer(acct.id, f.id, f.path, criteria)
1050
- .then(uids => ({ folderId: f.id, uids }))));
1051
- for (const r of results) {
1052
- if (r.status !== "fulfilled")
1053
- continue;
1054
- for (const uid of r.value.uids) {
1055
- const msg = this.db.getMessageByUid(acct.id, uid, r.value.folderId);
1056
- if (!msg)
1057
- continue;
1058
- const key = msg.messageId || `${acct.id}:${r.value.folderId}:${uid}`;
1059
- if (seen.has(key))
1067
+ for (let i = 0; i < folders.length; i += SERVER_SEARCH_BATCH) {
1068
+ const batch = folders.slice(i, i + SERVER_SEARCH_BATCH);
1069
+ const results = await Promise.allSettled(batch.map(f => this.imapManager.searchAndFetchOnServer(acct.id, f.id, f.path, criteria)
1070
+ .then(uids => ({ folder: f, uids }))));
1071
+ for (let j = 0; j < results.length; j++) {
1072
+ const r = results[j];
1073
+ if (r.status !== "fulfilled") {
1074
+ foldersFailed++;
1075
+ failedFolders.push(`${acct.id}/${batch[j].path}`);
1076
+ console.error(` [server-search] ${acct.id}/${batch[j].path}: ${r.reason?.message || r.reason}`);
1060
1077
  continue;
1061
- seen.add(key);
1062
- items.push(msg);
1063
- total++;
1078
+ }
1079
+ foldersSearched++;
1080
+ for (const uid of r.value.uids) {
1081
+ const msg = this.db.getMessageByUid(acct.id, uid, r.value.folder.id);
1082
+ if (!msg) {
1083
+ // SEARCH matched on the server but the
1084
+ // fetch-and-store in searchAndFetchOnServer
1085
+ // didn't land the row. Count it so the total
1086
+ // doesn't silently under-report.
1087
+ droppedHits++;
1088
+ console.error(` [server-search] ${acct.id}/${r.value.folder.path} uid ${uid}: SEARCH hit not in local DB after fetch — dropped`);
1089
+ continue;
1090
+ }
1091
+ const key = msg.messageId || `${acct.id}:${r.value.folder.id}:${uid}`;
1092
+ if (seen.has(key))
1093
+ continue;
1094
+ seen.add(key);
1095
+ items.push(msg);
1096
+ total++;
1097
+ }
1064
1098
  }
1065
1099
  }
1066
1100
  }
1067
1101
  // Newest first, then paginate.
1068
1102
  items.sort((a, b) => (b.date?.getTime?.() || 0) - (a.date?.getTime?.() || 0));
1069
1103
  const sliced = items.slice((page - 1) * pageSize, page * pageSize);
1070
- return { items: sliced, total, page, pageSize };
1104
+ if (foldersFailed > 0 || droppedHits > 0) {
1105
+ console.log(` [server-search] q="${q}" — ${total} hits across ${foldersSearched} folders; ${foldersFailed} folders failed, ${droppedHits} hits dropped`);
1106
+ }
1107
+ return {
1108
+ items: sliced, total, page, pageSize,
1109
+ partial: foldersFailed > 0 || droppedHits > 0,
1110
+ foldersSearched, foldersFailed, failedFolders, droppedHits,
1111
+ };
1071
1112
  }
1072
1113
  else if (scope === "current" && accountId && folderId) {
1073
1114
  // Per-folder search — folder scope is the user's explicit