@bobfrankston/rmfmail 1.2.77 → 1.2.79

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.
@@ -6753,7 +6753,11 @@ var GmailApiProvider = class _GmailApiProvider {
6753
6753
  }
6754
6754
  const cap = options.since ? 0 : 200;
6755
6755
  const ids = await this.listMessageIds(query, cap);
6756
- return this.batchFetch(ids, options);
6756
+ const fresh = options.knownUids ? ids.filter((id) => !options.knownUids.has(idToUid(id))) : ids;
6757
+ if (options.knownUids) {
6758
+ console.log(`[gmail] fetchSince ${folder}: ${ids.length} listed, ${ids.length - fresh.length} already stored, fetching ${fresh.length}`);
6759
+ }
6760
+ return this.batchFetch(fresh, options);
6757
6761
  }
6758
6762
  async fetchByDate(folder, since, before, options = {}, onChunk) {
6759
6763
  const afterDate = this.formatDate(since);
@@ -9796,6 +9800,11 @@ var AndroidSyncManager = class {
9796
9800
  // advanced to fetching new mail, so the list stayed stale despite "Synced"
9797
9801
  // (Bob 2026-06-27). Coalesce concurrent calls to one in-flight run.
9798
9802
  syncAllInflight = null;
9803
+ // Separate guard for the Phase-2/3 background pass (other folders + body
9804
+ // prefetch). It runs DETACHED from the inbox guard so a long folder sweep
9805
+ // can't make the 60s poll skip the inbox refresh (new mail must keep
9806
+ // flowing). One background pass at a time — overlapping triggers no-op.
9807
+ bgSyncInflight = null;
9799
9808
  on(_event, _handler) {
9800
9809
  }
9801
9810
  emit(event, ...args) {
@@ -9850,41 +9859,53 @@ var AndroidSyncManager = class {
9850
9859
  }
9851
9860
  async syncAll() {
9852
9861
  if (this.syncAllInflight) {
9853
- console.log("[sync] syncAll already in flight \u2014 coalescing this trigger");
9862
+ console.log("[sync] inbox sync already in flight \u2014 coalescing this trigger");
9854
9863
  return this.syncAllInflight;
9855
9864
  }
9856
9865
  const t0 = Date.now();
9857
- console.log("[sync] syncAll START");
9858
- this.syncAllInflight = this._syncAllImpl().catch((e) => {
9859
- console.error(`[sync] syncAll error: ${e?.message || e}`);
9866
+ console.log("[sync] syncAll START (inbox phase)");
9867
+ this.syncAllInflight = this._syncInboxes().catch((e) => {
9868
+ console.error(`[sync] inbox sync error: ${e?.message || e}`);
9860
9869
  }).finally(() => {
9861
- console.log(`[sync] syncAll DONE in ${Date.now() - t0}ms`);
9870
+ console.log(`[sync] inbox phase DONE in ${Date.now() - t0}ms`);
9862
9871
  this.syncAllInflight = null;
9863
9872
  });
9873
+ this._kickBackgroundSync();
9864
9874
  return this.syncAllInflight;
9865
9875
  }
9866
- async _syncAllImpl() {
9867
- const accounts = this.db.getAccounts();
9868
- vlog(`syncAll: ${accounts.length} accounts in DB: ${accounts.map((a) => a.id).join(",")}`);
9869
- for (const account of accounts) {
9870
- if (!this.providers.has(account.id)) continue;
9871
- try {
9872
- await withTimeout(`${account.id} inbox`, 6e4, async () => {
9873
- const folders = await this.syncFolders(account.id);
9874
- const inbox = folders.find((f) => f.specialUse === "inbox");
9875
- if (inbox) {
9876
- console.log(`[sync] ${account.id}: fetching INBOX (folderId=${inbox.id})`);
9877
- await this.syncFolder(account.id, inbox.id);
9878
- console.log(`[sync] ${account.id}: INBOX fetch done`);
9879
- emitEvent({ type: "syncComplete", accountId: account.id });
9880
- }
9881
- });
9882
- } catch (e) {
9883
- console.error(`[sync] ${account.id} inbox: ${e.message}`);
9884
- }
9885
- }
9876
+ /** Phase 1: sync every account's INBOX IN PARALLEL so a slow account (e.g.
9877
+ * Gmail's metadata fetch, or an auth-failing IMAP account) can't delay new
9878
+ * mail for the others. Each account is time-boxed; network fetches overlap
9879
+ * even though JS is single-threaded (the awaits yield). */
9880
+ async _syncInboxes() {
9881
+ const accounts = this.db.getAccounts().filter((a) => this.providers.has(a.id));
9882
+ vlog(`syncInboxes: ${accounts.length} accounts: ${accounts.map((a) => a.id).join(",")}`);
9883
+ await Promise.all(accounts.map(
9884
+ (account) => withTimeout(`${account.id} inbox`, 6e4, async () => {
9885
+ const folders = await this.syncFolders(account.id);
9886
+ const inbox = folders.find((f) => f.specialUse === "inbox");
9887
+ if (inbox) {
9888
+ console.log(`[sync] ${account.id}: fetching INBOX (folderId=${inbox.id})`);
9889
+ await this.syncFolder(account.id, inbox.id);
9890
+ console.log(`[sync] ${account.id}: INBOX fetch done`);
9891
+ emitEvent({ type: "syncComplete", accountId: account.id });
9892
+ }
9893
+ }).catch((e) => console.error(`[sync] ${account.id} inbox: ${e?.message || e}`))
9894
+ ));
9895
+ }
9896
+ /** Phase 2 + 3: remaining folders, then body prefetch. Runs in the
9897
+ * background under its own single-flight guard. */
9898
+ _kickBackgroundSync() {
9899
+ if (this.bgSyncInflight) return;
9900
+ const t0 = Date.now();
9901
+ this.bgSyncInflight = this._syncRemainingAndPrefetch().catch((e) => console.error(`[sync] background sweep error: ${e?.message || e}`)).finally(() => {
9902
+ console.log(`[sync] background sweep DONE in ${Date.now() - t0}ms`);
9903
+ this.bgSyncInflight = null;
9904
+ });
9905
+ }
9906
+ async _syncRemainingAndPrefetch() {
9907
+ const accounts = this.db.getAccounts().filter((a) => this.providers.has(a.id));
9886
9908
  for (const account of accounts) {
9887
- if (!this.providers.has(account.id)) continue;
9888
9909
  try {
9889
9910
  const folders = this.db.getFolders(account.id);
9890
9911
  const remaining = folders.filter((f) => f.specialUse !== "inbox");
@@ -9899,12 +9920,10 @@ var AndroidSyncManager = class {
9899
9920
  emitEvent({ type: "syncComplete", accountId: account.id });
9900
9921
  } catch (e) {
9901
9922
  console.error(`[sync] ${account.id}: ${e.message}`);
9902
- vlog(`syncAll: ${account.id} ERROR: ${e.message}`);
9903
9923
  emitEvent({ type: "syncError", accountId: account.id, error: e.message });
9904
9924
  }
9905
9925
  }
9906
9926
  for (const account of accounts) {
9907
- if (!this.providers.has(account.id)) continue;
9908
9927
  this.prefetchBodies(account.id).catch((e) => console.error(`[prefetch] ${account.id}: ${e.message}`));
9909
9928
  }
9910
9929
  }
@@ -10037,10 +10056,18 @@ var AndroidSyncManager = class {
10037
10056
  emitEvent({ type: "syncProgress", accountId, phase: `sync:${folder.path}`, progress: 0 });
10038
10057
  const highestUid = this.db.getHighestUid(accountId, folderId);
10039
10058
  const startDate = new Date(Date.now() - 30 * 864e5);
10059
+ const account = this.db.getAccounts().find((a) => a.id === accountId);
10060
+ const isGoogle = !!account && isGoogleAccount(account);
10040
10061
  let messages;
10041
10062
  if (highestUid > 0) {
10042
- messages = await provider.fetchSince(folder.path, highestUid, { source: false });
10043
- messages = messages.filter((m) => m.uid > highestUid);
10063
+ const opts = { source: false };
10064
+ if (isGoogle) {
10065
+ opts.knownUids = new Set(this.db.getUidsForFolder(accountId, folderId));
10066
+ }
10067
+ messages = await provider.fetchSince(folder.path, highestUid, opts);
10068
+ if (!isGoogle) {
10069
+ messages = messages.filter((m) => m.uid > highestUid);
10070
+ }
10044
10071
  } else {
10045
10072
  const tomorrow = new Date(Date.now() + 864e5);
10046
10073
  messages = await provider.fetchByDate(folder.path, startDate, tomorrow, { source: false });