@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.
- package/client/android-bootstrap.bundle.js +58 -31
- 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 +68 -41
- package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
- package/packages/mailx-store-web/android-bootstrap.ts +70 -39
- package/packages/mailx-store-web/package.json +1 -1
|
@@ -96,6 +96,11 @@ class AndroidSyncManager implements WebSyncManager {
|
|
|
96
96
|
// advanced to fetching new mail, so the list stayed stale despite "Synced"
|
|
97
97
|
// (Bob 2026-06-27). Coalesce concurrent calls to one in-flight run.
|
|
98
98
|
private syncAllInflight: Promise<void> | null = null;
|
|
99
|
+
// Separate guard for the Phase-2/3 background pass (other folders + body
|
|
100
|
+
// prefetch). It runs DETACHED from the inbox guard so a long folder sweep
|
|
101
|
+
// can't make the 60s poll skip the inbox refresh (new mail must keep
|
|
102
|
+
// flowing). One background pass at a time — overlapping triggers no-op.
|
|
103
|
+
private bgSyncInflight: Promise<void> | null = null;
|
|
99
104
|
|
|
100
105
|
constructor(
|
|
101
106
|
private db: WebMailxDB,
|
|
@@ -164,50 +169,65 @@ class AndroidSyncManager implements WebSyncManager {
|
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
async syncAll(): Promise<void> {
|
|
167
|
-
//
|
|
168
|
-
//
|
|
172
|
+
// The inbox guard covers ONLY Phase 1 (inbox sync). Overlapping triggers
|
|
173
|
+
// coalesce so they can't thrash the single main thread.
|
|
169
174
|
if (this.syncAllInflight) {
|
|
170
|
-
console.log("[sync]
|
|
175
|
+
console.log("[sync] inbox sync already in flight — coalescing this trigger");
|
|
171
176
|
return this.syncAllInflight;
|
|
172
177
|
}
|
|
173
178
|
const t0 = Date.now();
|
|
174
|
-
console.log("[sync] syncAll START");
|
|
175
|
-
this.syncAllInflight = this.
|
|
176
|
-
.catch((e: any) => { console.error(`[sync]
|
|
179
|
+
console.log("[sync] syncAll START (inbox phase)");
|
|
180
|
+
this.syncAllInflight = this._syncInboxes()
|
|
181
|
+
.catch((e: any) => { console.error(`[sync] inbox sync error: ${e?.message || e}`); })
|
|
177
182
|
.finally(() => {
|
|
178
|
-
console.log(`[sync]
|
|
183
|
+
console.log(`[sync] inbox phase DONE in ${Date.now() - t0}ms`);
|
|
179
184
|
this.syncAllInflight = null;
|
|
180
185
|
});
|
|
186
|
+
// Phase 2/3 (other folders + body prefetch) run DETACHED, under their own
|
|
187
|
+
// guard — they must not hold the inbox guard, or a slow folder sweep would
|
|
188
|
+
// make the next 60s poll skip new inbox mail.
|
|
189
|
+
this._kickBackgroundSync();
|
|
181
190
|
return this.syncAllInflight;
|
|
182
191
|
}
|
|
183
192
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
/** Phase 1: sync every account's INBOX IN PARALLEL so a slow account (e.g.
|
|
194
|
+
* Gmail's metadata fetch, or an auth-failing IMAP account) can't delay new
|
|
195
|
+
* mail for the others. Each account is time-boxed; network fetches overlap
|
|
196
|
+
* even though JS is single-threaded (the awaits yield). */
|
|
197
|
+
private async _syncInboxes(): Promise<void> {
|
|
198
|
+
const accounts = this.db.getAccounts().filter(a => this.providers.has(a.id));
|
|
199
|
+
vlog(`syncInboxes: ${accounts.length} accounts: ${accounts.map(a => a.id).join(",")}`);
|
|
200
|
+
await Promise.all(accounts.map(account =>
|
|
201
|
+
withTimeout(`${account.id} inbox`, 60000, async () => {
|
|
202
|
+
const folders = await this.syncFolders(account.id);
|
|
203
|
+
const inbox = folders.find(f => f.specialUse === "inbox");
|
|
204
|
+
if (inbox) {
|
|
205
|
+
console.log(`[sync] ${account.id}: fetching INBOX (folderId=${inbox.id})`);
|
|
206
|
+
await this.syncFolder(account.id, inbox.id);
|
|
207
|
+
console.log(`[sync] ${account.id}: INBOX fetch done`);
|
|
208
|
+
emitEvent({ type: "syncComplete", accountId: account.id });
|
|
209
|
+
}
|
|
210
|
+
}).catch((e: any) => console.error(`[sync] ${account.id} inbox: ${e?.message || e}`)),
|
|
211
|
+
));
|
|
212
|
+
}
|
|
187
213
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
emitEvent({ type: "syncComplete", accountId: account.id });
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
} catch (e: any) {
|
|
204
|
-
console.error(`[sync] ${account.id} inbox: ${e.message}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
214
|
+
/** Phase 2 + 3: remaining folders, then body prefetch. Runs in the
|
|
215
|
+
* background under its own single-flight guard. */
|
|
216
|
+
private _kickBackgroundSync(): void {
|
|
217
|
+
if (this.bgSyncInflight) return;
|
|
218
|
+
const t0 = Date.now();
|
|
219
|
+
this.bgSyncInflight = this._syncRemainingAndPrefetch()
|
|
220
|
+
.catch((e: any) => console.error(`[sync] background sweep error: ${e?.message || e}`))
|
|
221
|
+
.finally(() => {
|
|
222
|
+
console.log(`[sync] background sweep DONE in ${Date.now() - t0}ms`);
|
|
223
|
+
this.bgSyncInflight = null;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
207
226
|
|
|
208
|
-
|
|
227
|
+
private async _syncRemainingAndPrefetch(): Promise<void> {
|
|
228
|
+
const accounts = this.db.getAccounts().filter(a => this.providers.has(a.id));
|
|
229
|
+
// Phase 2: remaining folders (per account, per folder time-boxed).
|
|
209
230
|
for (const account of accounts) {
|
|
210
|
-
if (!this.providers.has(account.id)) continue;
|
|
211
231
|
try {
|
|
212
232
|
const folders = this.db.getFolders(account.id);
|
|
213
233
|
const remaining = folders.filter(f => f.specialUse !== "inbox");
|
|
@@ -219,16 +239,11 @@ class AndroidSyncManager implements WebSyncManager {
|
|
|
219
239
|
emitEvent({ type: "syncComplete", accountId: account.id });
|
|
220
240
|
} catch (e: any) {
|
|
221
241
|
console.error(`[sync] ${account.id}: ${e.message}`);
|
|
222
|
-
vlog(`syncAll: ${account.id} ERROR: ${e.message}`);
|
|
223
242
|
emitEvent({ type: "syncError", accountId: account.id, error: e.message });
|
|
224
243
|
}
|
|
225
244
|
}
|
|
226
|
-
|
|
227
|
-
// Phase 3: background body prefetch. Fire-and-forget — sync itself is
|
|
228
|
-
// already done and the UI doesn't wait on this. Per-account guard means
|
|
229
|
-
// a slow account can't block a fast one.
|
|
245
|
+
// Phase 3: body prefetch (fire-and-forget per account).
|
|
230
246
|
for (const account of accounts) {
|
|
231
|
-
if (!this.providers.has(account.id)) continue;
|
|
232
247
|
this.prefetchBodies(account.id).catch(e =>
|
|
233
248
|
console.error(`[prefetch] ${account.id}: ${e.message}`));
|
|
234
249
|
}
|
|
@@ -394,11 +409,27 @@ class AndroidSyncManager implements WebSyncManager {
|
|
|
394
409
|
emitEvent({ type: "syncProgress", accountId, phase: `sync:${folder.path}`, progress: 0 });
|
|
395
410
|
const highestUid = this.db.getHighestUid(accountId, folderId);
|
|
396
411
|
const startDate = new Date(Date.now() - 30 * 86400000);
|
|
412
|
+
const account = this.db.getAccounts().find(a => a.id === accountId);
|
|
413
|
+
const isGoogle = !!account && isGoogleAccount(account);
|
|
397
414
|
|
|
398
415
|
let messages: ProviderMessage[];
|
|
399
416
|
if (highestUid > 0) {
|
|
400
|
-
|
|
401
|
-
|
|
417
|
+
const opts: { source: boolean; knownUids?: Set<number> } = { source: false };
|
|
418
|
+
if (isGoogle) {
|
|
419
|
+
// Gmail re-lists a recent page (IDs aren't monotonic). Give it
|
|
420
|
+
// the set of UIDs we already have so it fetches only new ones
|
|
421
|
+
// instead of re-pulling ~200 every sync (Bob 2026-06-27).
|
|
422
|
+
opts.knownUids = new Set(this.db.getUidsForFolder(accountId, folderId));
|
|
423
|
+
}
|
|
424
|
+
messages = await provider.fetchSince(folder.path, highestUid, opts);
|
|
425
|
+
// IMAP UIDs are monotonic — keep the high-water guard. For Gmail the
|
|
426
|
+
// knownUids skip already returned only NEW messages; applying
|
|
427
|
+
// `uid > highestUid` to Gmail's HASH uids is a lottery that wrongly
|
|
428
|
+
// drops new mail whose hash falls below the mark (the long-standing
|
|
429
|
+
// "Gmail new messages missing" bug).
|
|
430
|
+
if (!isGoogle) {
|
|
431
|
+
messages = messages.filter(m => m.uid > highestUid);
|
|
432
|
+
}
|
|
402
433
|
} else {
|
|
403
434
|
const tomorrow = new Date(Date.now() + 86400000);
|
|
404
435
|
messages = await provider.fetchByDate(folder.path, startDate, tomorrow, { source: false });
|