Skip to content

Commit e44a557

Browse files
st3inybackportbot[bot]
authored andcommitted
perf: cache pre-fetched mailboxes on the HTTP level
Signed-off-by: Richard Steinmetz <[email protected]> [skip ci]
1 parent 85c2d95 commit e44a557

File tree

7 files changed

+221
-0
lines changed

7 files changed

+221
-0
lines changed

lib/Db/Mailbox.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ public function jsonSerialize() {
159159
'unread' => $this->unseen,
160160
'myAcls' => $this->myAcls,
161161
'shared' => $this->shared === true,
162+
'cacheBuster' => $this->getCacheBuster(),
162163
];
163164
}
164165
}

src/components/Mailbox.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ export default {
274274
const envelopes = await this.mainStore.fetchEnvelopes({
275275
mailboxId: mailbox.databaseId,
276276
limit: this.initialPageSize,
277+
includeCacheBuster: true,
277278
})
278279
this.syncedMailboxes.add(mailbox.databaseId)
279280
logger.debug(`Prefetched ${envelopes.length} envelopes for mailbox ${mailbox.displayName} (${mailbox.databaseId})`)

src/store/mainStore/actions.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,7 @@ export default function mainStoreActions() {
662662
mailboxId,
663663
query,
664664
addToUnifiedMailboxes = true,
665+
includeCacheBuster = false,
665666
}) {
666667
return handleHttpAuthErrors(async () => {
667668
const mailbox = this.getMailbox(mailboxId)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import * as MessageService from '../../../service/MessageService.js'
7+
import axios from '@nextcloud/axios'
8+
import { generateUrl } from '@nextcloud/router'
9+
10+
jest.mock('@nextcloud/axios')
11+
jest.mock('@nextcloud/router')
12+
13+
describe('service/MessageService test suite', () => {
14+
afterEach(() => {
15+
jest.clearAllMocks()
16+
})
17+
18+
it('should include a given cache buster as a URL parameter', async () => {
19+
generateUrl.mockReturnValueOnce('/generated-url')
20+
axios.get.mockResolvedValueOnce({ data: [] })
21+
22+
await MessageService.fetchEnvelopes(
23+
13, // account id
24+
21, // mailbox id
25+
undefined, // query
26+
undefined, // cursor
27+
undefined, // limit
28+
undefined, // sort ordre
29+
undefined, // layout
30+
'abcdef123', // cache buster
31+
)
32+
33+
expect(axios.get).toHaveBeenCalledWith('/generated-url', {
34+
params: {
35+
mailboxId: 21,
36+
v: 'abcdef123',
37+
},
38+
})
39+
})
40+
41+
it('should not include a cache buster by default', async () => {
42+
generateUrl.mockReturnValueOnce('/generated-url')
43+
axios.get.mockResolvedValueOnce({ data: [] })
44+
45+
await MessageService.fetchEnvelopes(
46+
13, // account id
47+
21, // mailbox id
48+
)
49+
50+
expect(axios.get).toHaveBeenCalledWith('/generated-url', {
51+
params: {
52+
mailboxId: 21,
53+
},
54+
})
55+
})
56+
})

src/tests/unit/store/actions.spec.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,4 +646,45 @@ describe('Vuex store actions', () => {
646646

647647
expect(removeEnvelope).toBeFalsy()
648648
})
649+
650+
it('includes a cache buster if requested', async() => {
651+
const account = {
652+
id: 13,
653+
personalNamespace: 'INBOX.',
654+
mailboxes: [],
655+
}
656+
657+
store.addAccountMutation(account)
658+
store.addMailboxMutation({
659+
account,
660+
mailbox: {
661+
id: 'INBOX',
662+
name: 'INBOX',
663+
databaseId: 21,
664+
accountId: 13,
665+
specialRole: 'inbox',
666+
cacheBuster: 'abcdef123',
667+
},
668+
})
669+
670+
store.addEnvelopesMutation = jest.fn()
671+
672+
MessageService.fetchEnvelopes.mockResolvedValueOnce([])
673+
674+
await store.fetchEnvelopes({
675+
mailboxId: 21,
676+
includeCacheBuster: true,
677+
})
678+
679+
expect(MessageService.fetchEnvelopes).toHaveBeenCalledWith(
680+
13, // account id
681+
21, // mailbox id
682+
undefined, // query
683+
undefined, // cursor
684+
20, // limit (PAGE_SIZE)
685+
undefined, // sort ordre
686+
undefined, // layout
687+
'abcdef123', // cache buster
688+
)
689+
})
649690
})

tests/Unit/Controller/MessagesControllerTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
use OCA\Mail\Contracts\IUserPreferences;
2525
use OCA\Mail\Controller\MessagesController;
2626
use OCA\Mail\Db\MailAccount;
27+
use OCA\Mail\Db\Mailbox;
28+
use OCA\Mail\Db\Message as DbMessage;
2729
use OCA\Mail\Db\Tag;
2830
use OCA\Mail\Exception\ClientException;
2931
use OCA\Mail\Exception\ServiceException;
@@ -1161,4 +1163,60 @@ public function testGetDkim() {
11611163
$this->assertInstanceOf(JSONResponse::class, $actualResponse);
11621164
$this->assertEquals(['valid' => true], $actualResponse->getData());
11631165
}
1166+
1167+
public static function provideCacheBusterData(): array {
1168+
return [
1169+
[null, false],
1170+
['', false],
1171+
['abcdef123', true],
1172+
];
1173+
}
1174+
1175+
/** @dataProvider provideCacheBusterData */
1176+
public function testIndexCacheBuster(?string $cacheBuster, bool $expectCaching): void {
1177+
$mailbox = new Mailbox();
1178+
$mailbox->setAccountId(100);
1179+
$this->mailManager->expects(self::once())
1180+
->method('getMailbox')
1181+
->with($this->userId, 100)
1182+
->willReturn($mailbox);
1183+
$mailAccount = new MailAccount();
1184+
$account = new Account($mailAccount);
1185+
$this->accountService->expects(self::once())
1186+
->method('find')
1187+
->with($this->userId, 100)
1188+
->willReturn($account);
1189+
1190+
$this->userPreferences->expects(self::once())
1191+
->method('getPreference')
1192+
->with($this->userId, 'sort-order', 'newest')
1193+
->willReturnArgument(2);
1194+
1195+
$messages = [
1196+
new DbMessage(),
1197+
new DbMessage(),
1198+
];
1199+
$this->mailSearch->expects(self::once())
1200+
->method('findMessages')
1201+
->with(
1202+
$account,
1203+
$mailbox,
1204+
'DESC',
1205+
null,
1206+
null,
1207+
null,
1208+
$this->userId,
1209+
'threaded',
1210+
)->willReturn($messages);
1211+
1212+
$actualResponse = $this->controller->index(100, null, null, null, null, $cacheBuster);
1213+
1214+
$cacheForHeader = $actualResponse->getHeaders()['Cache-Control'] ?? null;
1215+
$this->assertNotNull($cacheForHeader);
1216+
if ($expectCaching) {
1217+
$this->assertEquals('private, max-age=604800, immutable', $cacheForHeader);
1218+
} else {
1219+
$this->assertEquals('no-cache, no-store, must-revalidate', $cacheForHeader);
1220+
}
1221+
}
11641222
}

tests/Unit/Db/MailboxTest.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Mail\Tests\Unit\Db;
11+
12+
use ChristophWurst\Nextcloud\Testing\TestCase;
13+
use OCA\Mail\Db\Mailbox;
14+
15+
class MailboxTest extends TestCase {
16+
private Mailbox $mailbox;
17+
18+
protected function setUp(): void {
19+
parent::setUp();
20+
21+
$this->mailbox = new Mailbox();
22+
}
23+
24+
public static function provideCacheBusterData(): array {
25+
return [
26+
['new', 'changed', 'vanished', 'bbddae86e09069fc10c9f2ac401363b4'],
27+
[null, null, null, 'dca1f7641c34734a8cd1c7b1c45abf73'],
28+
];
29+
}
30+
31+
/** @dataProvider provideCacheBusterData */
32+
public function testGetCacheBuster(
33+
?string $syncNewToken,
34+
?string $syncChangedToken,
35+
?string $syncVanishedToken,
36+
string $expectedCacheBuster,
37+
): void {
38+
$this->mailbox->setId(100);
39+
$this->mailbox->setSyncNewToken($syncNewToken);
40+
$this->mailbox->setSyncChangedToken($syncChangedToken);
41+
$this->mailbox->setSyncVanishedToken($syncVanishedToken);
42+
43+
$this->assertEquals($expectedCacheBuster, $this->mailbox->getCacheBuster());
44+
}
45+
46+
/** @dataProvider provideCacheBusterData */
47+
public function testJsonSerializeCacheBuster(
48+
?string $syncNewToken,
49+
?string $syncChangedToken,
50+
?string $syncVanishedToken,
51+
string $expectedCacheBuster,
52+
): void {
53+
$this->mailbox->setId(100);
54+
$this->mailbox->setSyncNewToken($syncNewToken);
55+
$this->mailbox->setSyncChangedToken($syncChangedToken);
56+
$this->mailbox->setSyncVanishedToken($syncVanishedToken);
57+
$this->mailbox->setName('INBOX');
58+
59+
$json = $this->mailbox->jsonSerialize();
60+
$this->assertArrayHasKey('cacheBuster', $json);
61+
$this->assertEquals($expectedCacheBuster, $json['cacheBuster']);
62+
}
63+
}

0 commit comments

Comments
 (0)