1
10
11export interface Cursor {
12 position: string;
13 id: string;
14}
15
16export function encodeCursor(cursor: Cursor): string {
17 return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
18}
19
20export function decodeCursor(opaque: string): Cursor | null {
21 try {
22 const obj = JSON.parse(Buffer.from(opaque, "base64url").toString("utf8")) as {
23 position?: unknown;
24 id?: unknown;
25 };
26 if (typeof obj.position !== "string" || typeof obj.id !== "string") return null;
27 return { position: obj.position, id: obj.id };
28 } catch {
29 return null;
30 }
31}
32
33export interface Paged<T> {
34 items: T[];
35 cursor: string | null;
36 total: number;
37}
38
39interface Keyed {
40 position: string;
41 id: string;
42}
43
44
49export function paginateRows<T extends Keyed>(
50 rows: T[],
51 cursor: string | undefined,
52 pageSize: number,
53): Paged<T> {
54 const size = Math.min(Math.max(Math.trunc(pageSize) || 50, 1), 500);
55 let start = 0;
56 if (cursor) {
57 const c = decodeCursor(cursor);
58 if (c) {
59
60 start = rows.findIndex(
61 (r) => r.position > c.position || (r.position === c.position && r.id > c.id),
62 );
63 if (start < 0) start = rows.length;
64 }
65 }
66 const items = rows.slice(start, start + size);
67 const nextStart = start + items.length;
68 const hasMore = nextStart < rows.length;
69 const next = hasMore ? items[items.length - 1] : undefined;
70 return {
71 items,
72 cursor: next ? encodeCursor({ position: next.position, id: next.id }) : null,
73 total: rows.length,
74 };
75}
76