From f2356cdfc759e3748e18f745dc730c67a426ab09 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 3 Nov 2026 06:40:45 +0100 Subject: [PATCH] fix: look a send up soon after its response got lost, also with the server still there --- lib/offline/__tests__/outbox.test.ts | 7 +++++++ lib/offline/flush.ts | 3 +++ lib/offline/offline-mode.ts | 3 +++ 3 files changed, 13 insertions(+) diff --git a/lib/offline/__tests__/outbox.test.ts b/lib/offline/__tests__/outbox.test.ts index f2403a3a9..6d18aa28c 100644 --- a/lib/offline/__tests__/outbox.test.ts +++ b/lib/offline/__tests__/outbox.test.ts @@ -234,6 +234,13 @@ describe('the outbox', () => { // Not tried again on its own. await flushQueue('acc', cut as unknown as JMAPClient, { notify: vi.fn(), allowed: () => true }); expect(cut.sendEmail).toHaveBeenCalledTimes(1); + + // With the server still there, nothing else would start the lookup. + const entry = (await readQueue('acc')).outbox[0]; + await store.area('acc', 'outbox.update', { id: entry.id, status: 'waiting' }); + const retryLater = vi.fn(); + await flushQueue('acc', cut as unknown as JMAPClient, { notify: vi.fn(), allowed: () => true, retryLater }); + expect(retryLater).toHaveBeenCalledTimes(1); }); it('sends nothing once the organisation forbids offline mode', async () => { diff --git a/lib/offline/flush.ts b/lib/offline/flush.ts index bb4305781..021c39348 100644 --- a/lib/offline/flush.ts +++ b/lib/offline/flush.ts @@ -52,6 +52,8 @@ export interface FlushDeps { stricterNow?: (entry: OutboxEntry) => boolean; /** A held send went out (an undo window still open). */ onScheduled?: (entry: OutboxEntry, result: SendEmailResult) => void; + /** A send's connection broke though the server may be fine: look it up again soon. */ + retryLater?: () => void; } /** A send is taken for at most this long before a closed tab's send counts as uncertain. */ @@ -272,6 +274,7 @@ async function flushLocked(account: string, client: JMAPClient, deps: FlushDeps) if (isNetworkFailure(error) || (error instanceof Error && error.name === 'SendUnconfirmedError')) { // It may have reached the server: looked up before it goes again. await updateOutboxEntry(entry, { status: 'uncertain' }); + deps.retryLater?.(); return; } const text = error instanceof Error ? error.message : String(error); diff --git a/lib/offline/offline-mode.ts b/lib/offline/offline-mode.ts index 430fcd7af..ffb1c1fda 100644 --- a/lib/offline/offline-mode.ts +++ b/lib/offline/offline-mode.ts @@ -534,6 +534,8 @@ export function scheduleFlushAll(delayMs = 1_000): void { /** How long after a failed policy fetch the outbox asks again. */ const POLICY_RETRY_MS = 60_000; +/** A lost response with the server still there: nothing else would start the lookup. */ +const LOOKUP_AGAIN_MS = 30_000; /** A send held by the server for undo, from the outbox: the undo toast as after any send. */ function offerUndo(entry: OutboxEntry, result: SendEmailResult): void { @@ -578,6 +580,7 @@ export async function flushAll(): Promise { allowed: () => offlineAllowed(account), stricterNow, onScheduled: offerUndo, + retryLater: () => scheduleFlushAll(LOOKUP_AGAIN_MS), }).catch(() => {}); } await refreshQueueState();