@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.
- package/client/android-bootstrap.bundle.js +81 -13
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/package.json +3 -3
- package/packages/mailx-store-web/android-bootstrap.d.ts.map +1 -1
- package/packages/mailx-store-web/android-bootstrap.js +94 -27
- package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
- package/packages/mailx-store-web/android-bootstrap.ts +86 -19
- package/packages/mailx-store-web/package.json +1 -1
|
@@ -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
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
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
|
-
}
|
|
9830
|
-
|
|
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
|
}
|