/** How many pages a listing reads before it stops. At 100 records a page, that is 10,000 records. */ export const MAX_PAGES = 100; export interface Page { items: readonly T[]; cursor?: string | null; } /** * Yields every item across pages. Stops at an empty page, a missing cursor, a cursor the server already * sent, or after `maxPages`, so a server that repeats or cycles its cursor cannot keep the caller looping. * With `pageSize`, a page shorter than it is the last one: a PDS sends a cursor with every page that has * records, so without this every listing costs one more round trip for an empty page. */ export async function* paginate( fetchPage: (cursor: string | undefined) => Promise>, { maxPages = MAX_PAGES, pageSize }: { maxPages?: number; pageSize?: number } = {}, ): AsyncGenerator { const seen = new Set(); let cursor: string | undefined; for (let page = 0; page < maxPages; page++) { const { items, cursor: next } = await fetchPage(cursor); yield* items; if (items.length === 0 || (pageSize !== undefined && items.length < pageSize) || !next || seen.has(next)) return; seen.add(next); cursor = next; } }