fix: tell the user when a queued message gives up after unconfirmed attempts

This commit is contained in:
Linus Rath
2026-10-31 18:24:12 +01:00
parent e2d4915edc
commit 1c33f8daef
2 changed files with 39 additions and 3 deletions
@@ -154,6 +154,39 @@ describe('offline send queue', () => {
expect(useOutboxStore.getState().sends).toEqual([]);
});
it('says so when a message fails after too many unconfirmed attempts, as for a refused one', async () => {
useNetworkStore.setState({ online: true, connected: true });
runQueuedSend.mockRejectedValueOnce(new Error('Invalid envelope'));
useOutboxStore.getState().enqueueSend(payload('refused'));
await flushed();
await flushed();
expect(toast.error).toHaveBeenCalledTimes(1);
expect(toast.error.mock.calls[0][1]).toEqual({ message: 'refused: Invalid envelope' });
toast.error.mockClear();
const id = useOutboxStore.getState().enqueueSend(payload('unconfirmed'))!;
useOutboxStore.setState({ sends: useOutboxStore.getState().sends.map((s) => (s.id === id ? { ...s, attempts: 2 } : s)) });
runQueuedSend.mockRejectedValueOnce(named('SendUnconfirmedError', 'Send confirmation was not received'));
useOutboxStore.setState({ flushing: false });
await useOutboxStore.getState().flush();
await flushed();
expect(useOutboxStore.getState().sends.find((s) => s.id === id)).toMatchObject({ status: 'failed', attempts: 3 });
expect(toast.error).toHaveBeenCalledTimes(1);
expect(toast.error.mock.calls[0]).toEqual([
'A message in the Outbox could not be sent', { message: 'unconfirmed: Send confirmation was not received' },
]);
});
it('does not report an unconfirmed attempt that will be tried again', async () => {
useNetworkStore.setState({ online: true, connected: true });
runQueuedSend.mockRejectedValueOnce(named('SendUnconfirmedError', 'Send confirmation was not received'));
useOutboxStore.getState().enqueueSend(payload());
await flushed();
await flushed();
expect(useOutboxStore.getState().sends[0]).toMatchObject({ status: 'queued', attempts: 1 });
expect(toast.error).not.toHaveBeenCalled();
});
it('stops after an expired session without failing the message', async () => {
useNetworkStore.setState({ online: true, connected: true });
runQueuedSend.mockRejectedValueOnce(named('AuthenticationError', 'Session expired'));
+6 -3
View File
@@ -690,14 +690,17 @@ async function flushSends(accountId: string): Promise<'done' | 'transient'> {
} catch (err) {
const kind = classifySendFailure(err, isTransientNetworkError);
const message = err instanceof Error ? err.message : String(err);
updateSend(accountId, afterFailure(started, kind, message));
const next = afterFailure(started, kind, message);
updateSend(accountId, next);
// Refused, or unconfirmed too often: it waits for the user now, and
// nothing retries it, so say so whatever the reason.
if (next.status === 'failed') void notifyQueuedSendFailed(started, message);
if (kind === 'auth') {
useOutboxStore.setState({ paused: true });
break;
}
if (kind === 'offline' || kind === 'unconfirmed') return 'transient';
// Refused: this one waits for the user; the others go on.
void notifyQueuedSendFailed(started, message);
// Refused: the others go on.
}
}
return 'done';