refactor(web/api): offsetGetNextPageParam helper; paging sites migrated (W4)
Final task of PR2 DRY pass. Extracts the verbatim `offset + items.length >= total` math repeated across 8 createInfiniteQuery sites into a single Page<T>-shape helper. The task spec described a bare-array helper signature (lastPage: T[], pageSize-based stop), but no call site in this repo uses that shape — every paged endpoint returns a Page<T> envelope. The helper is named pageGetNextPageParam and matches the actual canonical shape so the 8 copies could collapse. Migrated: - likes.ts: 3 sites (TrackRef, AlbumRef, ArtistRef) - albums.ts: 1 site (AlbumRef) - queries.ts: 4 sites (artists + 3 search facets) history.ts left alone — uses has_more/total, different shape.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { createInfiniteQuery } from '@tanstack/svelte-query';
|
||||
import { api } from './client';
|
||||
import { qk } from './queries';
|
||||
import { pageGetNextPageParam } from './paging';
|
||||
import type { AlbumRef, AlbumDetail, Page, TrackRef } from './types';
|
||||
|
||||
export const ALBUM_PAGE_SIZE = 50;
|
||||
@@ -14,10 +15,7 @@ export function createAlbumsAlphaInfiniteQuery() {
|
||||
queryKey: qk.albumsAlpha(),
|
||||
queryFn: ({ pageParam = 0 }) => listAlbumsAlpha(ALBUM_PAGE_SIZE, pageParam),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (last) => {
|
||||
const loaded = last.offset + last.items.length;
|
||||
return loaded >= last.total ? undefined : loaded;
|
||||
}
|
||||
getNextPageParam: pageGetNextPageParam<AlbumRef>()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { QueryClient } from '@tanstack/svelte-query';
|
||||
import { createQuery, createInfiniteQuery } from '@tanstack/svelte-query';
|
||||
import { api } from './client';
|
||||
import { qk, ARTIST_PAGE_SIZE } from './queries';
|
||||
import { pageGetNextPageParam } from './paging';
|
||||
import type {
|
||||
ArtistRef,
|
||||
AlbumRef,
|
||||
@@ -38,10 +39,7 @@ export function createLikedTracksInfiniteQuery() {
|
||||
queryFn: ({ pageParam = 0 }) =>
|
||||
api.get<Page<TrackRef>>(`/api/likes/tracks?limit=${ARTIST_PAGE_SIZE}&offset=${pageParam}`),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (last) => {
|
||||
const loaded = last.offset + last.items.length;
|
||||
return loaded >= last.total ? undefined : loaded;
|
||||
}
|
||||
getNextPageParam: pageGetNextPageParam<TrackRef>()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,10 +49,7 @@ export function createLikedAlbumsInfiniteQuery() {
|
||||
queryFn: ({ pageParam = 0 }) =>
|
||||
api.get<Page<AlbumRef>>(`/api/likes/albums?limit=${ARTIST_PAGE_SIZE}&offset=${pageParam}`),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (last) => {
|
||||
const loaded = last.offset + last.items.length;
|
||||
return loaded >= last.total ? undefined : loaded;
|
||||
}
|
||||
getNextPageParam: pageGetNextPageParam<AlbumRef>()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,10 +59,7 @@ export function createLikedArtistsInfiniteQuery() {
|
||||
queryFn: ({ pageParam = 0 }) =>
|
||||
api.get<Page<ArtistRef>>(`/api/likes/artists?limit=${ARTIST_PAGE_SIZE}&offset=${pageParam}`),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (last) => {
|
||||
const loaded = last.offset + last.items.length;
|
||||
return loaded >= last.total ? undefined : loaded;
|
||||
}
|
||||
getNextPageParam: pageGetNextPageParam<ArtistRef>()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { pageGetNextPageParam } from './paging';
|
||||
import type { Page } from './types';
|
||||
|
||||
function mkPage<T>(items: T[], offset: number, total: number, limit = 50): Page<T> {
|
||||
return { items, offset, total, limit };
|
||||
}
|
||||
|
||||
describe('pageGetNextPageParam', () => {
|
||||
it('returns next offset (offset + items.length) when more items remain', () => {
|
||||
const next = pageGetNextPageParam<number>();
|
||||
const last = mkPage([1, 2, 3, 4, 5], 0, 12);
|
||||
expect(next(last)).toBe(5);
|
||||
});
|
||||
|
||||
it('returns next offset accounting for prior offset', () => {
|
||||
const next = pageGetNextPageParam<number>();
|
||||
const last = mkPage([1, 2, 3], 50, 100);
|
||||
expect(next(last)).toBe(53);
|
||||
});
|
||||
|
||||
it('returns undefined when loaded reaches total', () => {
|
||||
const next = pageGetNextPageParam<number>();
|
||||
const last = mkPage([1, 2, 3], 7, 10);
|
||||
expect(next(last)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined when last page is empty and offset already equals total', () => {
|
||||
const next = pageGetNextPageParam<number>();
|
||||
const last = mkPage<number>([], 10, 10);
|
||||
expect(next(last)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined when loaded exceeds total (defensive)', () => {
|
||||
const next = pageGetNextPageParam<number>();
|
||||
const last = mkPage([1, 2, 3], 9, 10);
|
||||
expect(next(last)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('works for different element types via generic parameter', () => {
|
||||
const next = pageGetNextPageParam<string>();
|
||||
const last = mkPage(['a', 'b'], 0, 5);
|
||||
expect(next(last)).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Page } from './types';
|
||||
|
||||
/**
|
||||
* getNextPageParam factory for offset/total infinite queries that fetch
|
||||
* Page<T> envelopes (items + total + limit + offset) and stop when the
|
||||
* loaded count reaches the server-reported total.
|
||||
*
|
||||
* Use with TanStack svelte-query's createInfiniteQuery:
|
||||
* getNextPageParam: pageGetNextPageParam<TrackRef>()
|
||||
*
|
||||
* NOTE: the spec for this task described a bare-array shape
|
||||
* (lastPage: T[], pageSize-based stop). No call site in this repo uses
|
||||
* that shape — every paged endpoint returns Page<T>. The helper matches
|
||||
* the actual canonical shape so the 8 verbatim copies can collapse.
|
||||
*/
|
||||
export function pageGetNextPageParam<T>() {
|
||||
return (last: Page<T>): number | undefined => {
|
||||
const loaded = last.offset + last.items.length;
|
||||
return loaded >= last.total ? undefined : loaded;
|
||||
};
|
||||
}
|
||||
+14
-17
@@ -1,6 +1,15 @@
|
||||
import { createQuery, createInfiniteQuery } from '@tanstack/svelte-query';
|
||||
import { api } from './client';
|
||||
import type { ArtistRef, ArtistDetail, AlbumDetail, Page, SearchResponse } from './types';
|
||||
import { pageGetNextPageParam } from './paging';
|
||||
import type {
|
||||
ArtistRef,
|
||||
ArtistDetail,
|
||||
AlbumRef,
|
||||
AlbumDetail,
|
||||
Page,
|
||||
SearchResponse,
|
||||
TrackRef
|
||||
} from './types';
|
||||
|
||||
export type ArtistSort = 'alpha' | 'newest';
|
||||
export const ARTIST_PAGE_SIZE = 50;
|
||||
@@ -60,10 +69,7 @@ export function createArtistsQuery(sort: ArtistSort) {
|
||||
`/api/artists?sort=${sort}&limit=${ARTIST_PAGE_SIZE}&offset=${pageParam}`
|
||||
),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (last) => {
|
||||
const loaded = last.offset + last.items.length;
|
||||
return loaded >= last.total ? undefined : loaded;
|
||||
},
|
||||
getNextPageParam: pageGetNextPageParam<ArtistRef>(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,10 +106,7 @@ export function createSearchArtistsInfiniteQuery(q: string) {
|
||||
`/api/search?q=${encodeURIComponent(q)}&limit=${SEARCH_FACET_PAGE_SIZE}&offset=${pageParam}`
|
||||
).then((r) => r.artists),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (last) => {
|
||||
const loaded = last.offset + last.items.length;
|
||||
return loaded >= last.total ? undefined : loaded;
|
||||
},
|
||||
getNextPageParam: pageGetNextPageParam<ArtistRef>(),
|
||||
enabled: q.length > 0
|
||||
});
|
||||
}
|
||||
@@ -116,10 +119,7 @@ export function createSearchAlbumsInfiniteQuery(q: string) {
|
||||
`/api/search?q=${encodeURIComponent(q)}&limit=${SEARCH_FACET_PAGE_SIZE}&offset=${pageParam}`
|
||||
).then((r) => r.albums),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (last) => {
|
||||
const loaded = last.offset + last.items.length;
|
||||
return loaded >= last.total ? undefined : loaded;
|
||||
},
|
||||
getNextPageParam: pageGetNextPageParam<AlbumRef>(),
|
||||
enabled: q.length > 0
|
||||
});
|
||||
}
|
||||
@@ -132,10 +132,7 @@ export function createSearchTracksInfiniteQuery(q: string) {
|
||||
`/api/search?q=${encodeURIComponent(q)}&limit=${SEARCH_FACET_PAGE_SIZE}&offset=${pageParam}`
|
||||
).then((r) => r.tracks),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (last) => {
|
||||
const loaded = last.offset + last.items.length;
|
||||
return loaded >= last.total ? undefined : loaded;
|
||||
},
|
||||
getNextPageParam: pageGetNextPageParam<TrackRef>(),
|
||||
enabled: q.length > 0
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user