@bobfrankston/rmfmail 1.2.76 → 1.2.78

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.
@@ -9749,6 +9749,32 @@ function vlog(msg) {
9749
9749
  } catch {
9750
9750
  }
9751
9751
  }
9752
+ function withTimeout(label, ms, fn) {
9753
+ return new Promise((resolve, reject) => {
9754
+ let done = false;
9755
+ const timer = setTimeout(() => {
9756
+ if (done) return;
9757
+ done = true;
9758
+ reject(new Error(`timeout after ${ms}ms: ${label}`));
9759
+ }, ms);
9760
+ fn().then(
9761
+ (v) => {
9762
+ if (!done) {
9763
+ done = true;
9764
+ clearTimeout(timer);
9765
+ resolve(v);
9766
+ }
9767
+ },
9768
+ (e) => {
9769
+ if (!done) {
9770
+ done = true;
9771
+ clearTimeout(timer);
9772
+ reject(e);
9773
+ }
9774
+ }
9775
+ );
9776
+ });
9777
+ }
9752
9778
  var AndroidSyncManager = class {
9753
9779
  constructor(db2, bodyStore2) {
9754
9780
  this.db = db2;
@@ -9762,6 +9788,19 @@ var AndroidSyncManager = class {
9762
9788
  // spawning parallel fetch loops that race on IndexedDB and blow through
9763
9789
  // Gmail's per-user quota.
9764
9790
  prefetchingAccounts = /* @__PURE__ */ new Set();
9791
+ // Single-flight guard for syncAll. It's triggered from THREE places — boot,
9792
+ // a 60s interval, and every visibilitychange→visible — with no coordination.
9793
+ // On Android everything (incl. wa-sqlite) runs on the main thread, so
9794
+ // overlapping runs stacked up: re-listing folders repeatedly, contending on
9795
+ // the DB, and freezing the event loop ~1s at a time — the sync never
9796
+ // advanced to fetching new mail, so the list stayed stale despite "Synced"
9797
+ // (Bob 2026-06-27). Coalesce concurrent calls to one in-flight run.
9798
+ syncAllInflight = null;
9799
+ // Separate guard for the Phase-2/3 background pass (other folders + body
9800
+ // prefetch). It runs DETACHED from the inbox guard so a long folder sweep
9801
+ // can't make the 60s poll skip the inbox refresh (new mail must keep
9802
+ // flowing). One background pass at a time — overlapping triggers no-op.
9803
+ bgSyncInflight = null;
9765
9804
  on(_event, _handler) {
9766
9805
  }
9767
9806
  emit(event, ...args) {
@@ -9815,29 +9854,60 @@ var AndroidSyncManager = class {
9815
9854
  return this.providers.get(accountId) || null;
9816
9855
  }
9817
9856
  async syncAll() {
9818
- const accounts = this.db.getAccounts();
9819
- vlog(`syncAll: ${accounts.length} accounts in DB: ${accounts.map((a) => a.id).join(",")}`);
9820
- for (const account of accounts) {
9821
- if (!this.providers.has(account.id)) continue;
9822
- try {
9857
+ if (this.syncAllInflight) {
9858
+ console.log("[sync] inbox sync already in flight \u2014 coalescing this trigger");
9859
+ return this.syncAllInflight;
9860
+ }
9861
+ const t0 = Date.now();
9862
+ console.log("[sync] syncAll START (inbox phase)");
9863
+ this.syncAllInflight = this._syncInboxes().catch((e) => {
9864
+ console.error(`[sync] inbox sync error: ${e?.message || e}`);
9865
+ }).finally(() => {
9866
+ console.log(`[sync] inbox phase DONE in ${Date.now() - t0}ms`);
9867
+ this.syncAllInflight = null;
9868
+ });
9869
+ this._kickBackgroundSync();
9870
+ return this.syncAllInflight;
9871
+ }
9872
+ /** Phase 1: sync every account's INBOX IN PARALLEL so a slow account (e.g.
9873
+ * Gmail's metadata fetch, or an auth-failing IMAP account) can't delay new
9874
+ * mail for the others. Each account is time-boxed; network fetches overlap
9875
+ * even though JS is single-threaded (the awaits yield). */
9876
+ async _syncInboxes() {
9877
+ const accounts = this.db.getAccounts().filter((a) => this.providers.has(a.id));
9878
+ vlog(`syncInboxes: ${accounts.length} accounts: ${accounts.map((a) => a.id).join(",")}`);
9879
+ await Promise.all(accounts.map(
9880
+ (account) => withTimeout(`${account.id} inbox`, 6e4, async () => {
9823
9881
  const folders = await this.syncFolders(account.id);
9824
9882
  const inbox = folders.find((f) => f.specialUse === "inbox");
9825
9883
  if (inbox) {
9884
+ console.log(`[sync] ${account.id}: fetching INBOX (folderId=${inbox.id})`);
9826
9885
  await this.syncFolder(account.id, inbox.id);
9886
+ console.log(`[sync] ${account.id}: INBOX fetch done`);
9827
9887
  emitEvent({ type: "syncComplete", accountId: account.id });
9828
9888
  }
9829
- } catch (e) {
9830
- console.error(`[sync] ${account.id} inbox: ${e.message}`);
9831
- }
9832
- }
9889
+ }).catch((e) => console.error(`[sync] ${account.id} inbox: ${e?.message || e}`))
9890
+ ));
9891
+ }
9892
+ /** Phase 2 + 3: remaining folders, then body prefetch. Runs in the
9893
+ * background under its own single-flight guard. */
9894
+ _kickBackgroundSync() {
9895
+ if (this.bgSyncInflight) return;
9896
+ const t0 = Date.now();
9897
+ this.bgSyncInflight = this._syncRemainingAndPrefetch().catch((e) => console.error(`[sync] background sweep error: ${e?.message || e}`)).finally(() => {
9898
+ console.log(`[sync] background sweep DONE in ${Date.now() - t0}ms`);
9899
+ this.bgSyncInflight = null;
9900
+ });
9901
+ }
9902
+ async _syncRemainingAndPrefetch() {
9903
+ const accounts = this.db.getAccounts().filter((a) => this.providers.has(a.id));
9833
9904
  for (const account of accounts) {
9834
- if (!this.providers.has(account.id)) continue;
9835
9905
  try {
9836
9906
  const folders = this.db.getFolders(account.id);
9837
9907
  const remaining = folders.filter((f) => f.specialUse !== "inbox");
9838
9908
  for (const folder of remaining) {
9839
9909
  try {
9840
- await this.syncFolder(account.id, folder.id);
9910
+ await withTimeout(`${account.id} ${folder.path}`, 45e3, () => this.syncFolder(account.id, folder.id));
9841
9911
  } catch (e) {
9842
9912
  console.error(`[sync] Skip ${folder.path}: ${e.message}`);
9843
9913
  }
@@ -9846,12 +9916,10 @@ var AndroidSyncManager = class {
9846
9916
  emitEvent({ type: "syncComplete", accountId: account.id });
9847
9917
  } catch (e) {
9848
9918
  console.error(`[sync] ${account.id}: ${e.message}`);
9849
- vlog(`syncAll: ${account.id} ERROR: ${e.message}`);
9850
9919
  emitEvent({ type: "syncError", accountId: account.id, error: e.message });
9851
9920
  }
9852
9921
  }
9853
9922
  for (const account of accounts) {
9854
- if (!this.providers.has(account.id)) continue;
9855
9923
  this.prefetchBodies(account.id).catch((e) => console.error(`[prefetch] ${account.id}: ${e.message}`));
9856
9924
  }
9857
9925
  }