diff --git a/src/store/mainStore/actions.js b/src/store/mainStore/actions.js index 825551d174..f967d2882d 100644 --- a/src/store/mainStore/actions.js +++ b/src/store/mainStore/actions.js @@ -722,6 +722,12 @@ export default function mainStoreActions() { const mailbox = this.getMailbox(mailboxId) if (mailbox.isUnified) { + // One account's fetch rejecting must not discard every + // other account's already-successful envelopes. Promise.all() + // rejects as soon as any single promise rejects, so a single + // slow/unreachable account used to make the whole unified + // mailbox render nothing at all instead of everything except + // that one account. See #9072. const fetchIndividualLists = pipe( map((mb) => this.fetchEnvelopes({ mailboxId: mb.databaseId, @@ -729,6 +735,9 @@ export default function mainStoreActions() { addToUnifiedMailboxes: false, sort: this.getPreference('sort-order'), view: this.getPreference('layout-message-view'), + }).catch((error) => { + logger.error(`Failed to fetch envelopes for unified constituent mailbox ${mb.databaseId}: ${error}`, { error }) + return [] })), Promise.all.bind(Promise), andThen(map(sliceToPage)), @@ -825,12 +834,18 @@ export default function mainStoreActions() { logger.debug('not enough local envelopes for the next unified page. ' + mbs.length + ' fetches required', { mailboxes: mbs.map((mb) => mb.databaseId), }) + // Same reasoning as fetchEnvelopes() above: one account + // failing must not fail pagination for every other + // account sharing this unified mailbox. return pipe( map((mb) => this.fetchNextEnvelopes({ mailboxId: mb.databaseId, query, quantity, addToUnifiedMailboxes: false, + }).catch((error) => { + logger.error(`Failed to fetch next envelopes for unified constituent mailbox ${mb.databaseId}: ${error}`, { error }) + return [] })), Promise.all.bind(Promise), andThen(() => this.fetchNextEnvelopes({ diff --git a/src/tests/unit/store/actions.spec.js b/src/tests/unit/store/actions.spec.js index 1b57709a23..3ea96a3f75 100644 --- a/src/tests/unit/store/actions.spec.js +++ b/src/tests/unit/store/actions.spec.js @@ -197,6 +197,73 @@ describe('Vuex store actions', () => { }) }) + it('creates a unified page from the accounts that succeed even if another account fails', async () => { + const account13 = { + id: 13, + personalNamespace: '', + mailboxes: [], + } + const account14 = { + id: 14, + personalNamespace: '', + mailboxes: [], + } + + store.addAccountMutation(account13) + store.addAccountMutation(account14) + store.addMailboxMutation({ + account: account13, + mailbox: { + id: 'INBOX', + name: 'INBOX', + databaseId: 21, + accountId: 13, + specialRole: 'inbox', + }, + }) + store.addMailboxMutation({ + account: account14, + mailbox: { + id: 'INBOX', + name: 'INBOX', + databaseId: 31, + accountId: 14, + specialRole: 'inbox', + }, + }) + + store.addEnvelopesMutation = vi.fn() + + MessageService.fetchEnvelopes.mockImplementation(async (accountId) => { + if (accountId === 14) { + throw new Error('account 14 is temporarily unavailable') + } + + return [{ + databaseId: 123, + mailboxId: 21, + uid: 321, + subject: 'msg1', + }] + }) + + const envelopes = await store.fetchEnvelopes({ + mailboxId: UNIFIED_INBOX_ID, + }) + + // The unreachable account (14) contributes nothing, but the + // reachable one (13) still renders instead of the whole unified + // fetch coming back empty. + expect(envelopes).toEqual([ + { + databaseId: 123, + mailboxId: 21, + uid: 321, + subject: 'msg1', + }, + ]) + }) + it('fetches the next individual page', async () => { const msgs1 = reverse(range(30, 40)) const page1 = reverse(range(10, 30))