This repository has no description
1export interface CursorPage<T, C> {
2 items: T[];
3 cursor?: C;
4}
5
6type CursorPagerState =
7 | { kind: "at-page"; page: number }
8 | { kind: "loading"; page: number }
9 | { kind: "failed"; page: number };
10
11export const pageCount = (total: number, limit: number): number =>
12 Math.max(1, Math.ceil(Math.max(total, 0) / limit));
13
14// search uses this since there isn't a known limit
15export const discoveredPageCount = (page: number, hasNext: boolean): number =>
16 Math.max(1, page + (hasNext ? 1 : 0));
17
18export const createCursorPager = <T, C>(
19 initial: CursorPage<T, C>,
20 load: (cursor: C) => Promise<CursorPage<T, C>>,
21 hasNext: (cursor: C | undefined) => boolean = (cursor) => cursor !== undefined
22) => {
23 let state = $state<CursorPagerState>({ kind: "at-page", page: 1 });
24 let pages = $state<Record<number, CursorPage<T, C>>>({ 1: initial });
25
26 const select = async (target: number) => {
27 if (state.kind === "loading" || target < 1 || target === state.page) return;
28
29 const currentPage = state.page;
30 state = { kind: "loading", page: currentPage };
31 try {
32 const loaded = { ...pages };
33 for (let next = 2; next <= target; next++) {
34 if (loaded[next]) continue;
35 const cursor = loaded[next - 1]?.cursor;
36 if (!hasNext(cursor)) break;
37 loaded[next] = await load(cursor as C);
38 }
39
40 pages = loaded;
41 state = { kind: "at-page", page: loaded[target] ? target : currentPage };
42 } catch {
43 state = { kind: "failed", page: currentPage };
44 }
45 };
46
47 return {
48 get state() {
49 return state;
50 },
51 get items() {
52 return pages[state.page]?.items ?? [];
53 },
54 get hasNext() {
55 return hasNext(pages[state.page]?.cursor);
56 },
57 select
58 };
59};