fix: page through all chapters when the API omits total
All checks were successful
deploy / test-backend (push) Successful in 44m20s
deploy / test-frontend (push) Successful in 11m3s
deploy / build-and-push (push) Successful in 13m53s
deploy / deploy (push) Successful in 20s

listAllChapters used `page.total ?? all.length` as the loop bound. The
/chapters endpoint always returns `total: null`, so a full 200-row first
page broke the loop immediately — the reader dead-ended past chapter 200.
Stop on a short page instead; only trust `total` when actually reported.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-11 16:24:49 +02:00
parent f8e53809d5
commit a44511983d
5 changed files with 25 additions and 5 deletions

2
backend/Cargo.lock generated
View File

@@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mangalord"
version = "0.128.3"
version = "0.128.4"
dependencies = [
"anyhow",
"argon2",

View File

@@ -1,6 +1,6 @@
[package]
name = "mangalord"
version = "0.128.3"
version = "0.128.4"
edition = "2021"
default-run = "mangalord"

View File

@@ -1,6 +1,6 @@
{
"name": "mangalord-frontend",
"version": "0.128.3",
"version": "0.128.4",
"private": true,
"type": "module",
"scripts": {

View File

@@ -158,6 +158,23 @@ describe('chapters api client', () => {
expect(fetchSpy.mock.calls[1][0]).toContain('offset=200');
});
it('listAllChapters keeps paging when the API omits total (total: null)', async () => {
// The real /chapters endpoint returns `total: null` on every page
// (PagedResponse::new). A full 200-row first page must NOT be mistaken
// for the end — otherwise the reader dead-ends past chapter 200.
const mk = (n: number) => ({ ...chapterFixture, id: `c${n}`, number: n });
const first = Array.from({ length: 200 }, (_, i) => mk(i + 1));
const second = Array.from({ length: 50 }, (_, i) => mk(i + 201));
fetchSpy
.mockResolvedValueOnce(ok({ items: first, page: { limit: 200, offset: 0, total: null } }))
.mockResolvedValueOnce(ok({ items: second, page: { limit: 200, offset: 200, total: null } }));
const all = await listAllChapters('m1');
expect(all).toHaveLength(250);
expect(all[249].number).toBe(250);
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
it('listAllChapters stops after a short final page (single request)', async () => {
fetchSpy.mockResolvedValueOnce(
ok({ items: [chapterFixture], page: { limit: 200, offset: 0, total: 1 } })

View File

@@ -56,8 +56,11 @@ export async function listAllChapters(mangaId: string): Promise<Chapter[]> {
const { items, page } = await listChapters(mangaId, { limit: PAGE, offset });
all.push(...items);
offset += items.length;
const total = page.total ?? all.length;
if (items.length < PAGE || all.length >= total) break;
// A short page is the only reliable end-of-list signal: the /chapters
// endpoint returns `total: null`, so we can't infer completion from it.
// Only stop early on `total` when the API actually reports one.
if (items.length < PAGE) break;
if (page.total != null && all.length >= page.total) break;
}
return all;
}