TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1/**2 * Pagination helpers shared by connectors (SPEC §1: page numbers, cursors, offsets, infinite scroll3 * backed by an XHR endpoint). They are deliberately tiny: the connector decides how to build a URL4 * for a page and how to detect the end; these helpers handle bounds, cursors and abort signals.5 */67export interface PageLoopOptions {8 /** first page index (1 for most sites, 0 for offset APIs) */9 start?: number;10 /** hard cap of pages to visit */11 maxPages: number;12 signal?: AbortSignal;13}1415/**16 * Iterate numbered pages until `load` returns `done: true` or the cap is reached.17 * `load` receives the page index and returns the items plus a done flag.18 */19export async function* numberedPages<T>(load: (page: number) => Promise<{ items: T[]; done?: boolean }>, opts: PageLoopOptions): AsyncGenerator<{ page: number; items: T[] }> {20 const start = opts.start ?? 1;21 for (let page = start; page < start + opts.maxPages; page++) {22 if (opts.signal?.aborted) return;23 const r = await load(page);24 yield { page, items: r.items };25 if (r.done || r.items.length === 0) return;26 }27}2829/**30 * Cursor pagination: `load(cursor)` returns items and the next cursor (null when finished).31 * Resumable: pass the persisted cursor as `startCursor`.32 */33export async function* cursorPages<T, C = string>(load: (cursor: C | null) => Promise<{ items: T[]; next: C | null }>, opts: { startCursor?: C | null; maxPages: number; signal?: AbortSignal }): AsyncGenerator<{ cursor: C | null; next: C | null; items: T[] }> {34 let cursor: C | null = opts.startCursor ?? null;35 for (let i = 0; i < opts.maxPages; i++) {36 if (opts.signal?.aborted) return;37 const r = await load(cursor);38 yield { cursor, next: r.next, items: r.items };39 if (r.next === null || r.items.length === 0) return;40 cursor = r.next;41 }42}4344/** Offset pagination (limit/offset APIs behind infinite-scroll pages). */45export async function* offsetPages<T>(load: (offset: number, limit: number) => Promise<{ items: T[]; total?: number | null }>, opts: { limit: number; startOffset?: number; maxPages: number; signal?: AbortSignal }): AsyncGenerator<{ offset: number; items: T[]; total: number | null }> {46 let offset = opts.startOffset ?? 0;47 for (let i = 0; i < opts.maxPages; i++) {48 if (opts.signal?.aborted) return;49 const r = await load(offset, opts.limit);50 const total = r.total ?? null;51 yield { offset, items: r.items, total };52 offset += opts.limit;53 if (r.items.length < opts.limit || (total !== null && offset >= total)) return;54 }55}5657/** Build a URL with query parameters merged onto a base (keeps existing params). */58export function withParams(base: string, params: Record<string, string | number | boolean | null | undefined>): string {59 const u = new URL(base);60 for (const [k, v] of Object.entries(params)) {61 if (v === null || v === undefined) u.searchParams.delete(k);62 else u.searchParams.set(k, String(v));63 }64 return u.toString();65}66