import type { XRPCQueries } from "@atcute/lexicons/ambient"; import type { InferInput, InferOutput, ObjectSchema, XRPCLexBodyParam } from "@atcute/lexicons/validations"; import { ok, type BobbinContext, type XrpcRequestInit } from "./client"; export type QueryOutput = XRPCQueries[TName]["output"] extends XRPCLexBodyParam ? InferOutput : never; export type QueryParams = XRPCQueries[TName]["params"] extends ObjectSchema ? InferInput : Record; type CursorPage = { items: readonly unknown[]; cursor?: string }; // registered queries whose output is an { items, cursor? } page. export type PaginatedQuery = { [K in keyof XRPCQueries]: QueryOutput extends CursorPage ? K : never; }[keyof XRPCQueries]; export type PageItem = QueryOutput extends { items: readonly (infer I)[] } ? I : never; export type PageParams = Omit, "cursor">; export interface PaginateOptions extends XrpcRequestInit { maxPages?: number; } const requestPage = ( ctx: BobbinContext, name: TName, params: PageParams & { cursor?: string }, init?: XrpcRequestInit ): Promise> => ok( ctx.xrpc.get(name, { params, signal: init?.signal, headers: init?.headers } as never) ) as Promise>; export const fetchPage = ( ctx: BobbinContext, name: TName, params: PageParams & { cursor?: string }, init?: XrpcRequestInit ): Promise> => requestPage(ctx, name, params, init); export async function* pages( ctx: BobbinContext, name: TName, params: PageParams, options: PaginateOptions = {} ): AsyncGenerator, void, unknown> { let cursor: string | undefined; let seen = 0; do { const data = await requestPage( ctx, name, { ...params, cursor } as PageParams & { cursor?: string }, options ); yield data; const next = (data as CursorPage).cursor; if (next !== undefined && next === cursor) break; cursor = next; if (options.maxPages && ++seen >= options.maxPages) break; } while (cursor); } export async function* items( ctx: BobbinContext, name: TName, params: PageParams, options: PaginateOptions = {} ): AsyncGenerator, void, unknown> { for await (const data of pages(ctx, name, params, options)) { for (const item of (data as CursorPage).items) yield item as PageItem; } } export interface CollectOptions extends PaginateOptions { max?: number; } export const collect = async ( ctx: BobbinContext, name: TName, params: PageParams, options: CollectOptions = {} ): Promise[]> => { const out: PageItem[] = []; for await (const item of items(ctx, name, params, options)) { out.push(item); if (options.max && out.length >= options.max) break; } return out; }; export async function* paginateBy( load: (cursor: string | undefined) => Promise<{ items: readonly T[]; cursor?: string | null }>, options: { maxPages?: number } = {} ): AsyncGenerator { let cursor: string | undefined; let seen = 0; do { const page = await load(cursor); for (const item of page.items) yield item; const next = page.cursor ?? undefined; if (next !== undefined && next === cursor) break; cursor = next; if (options.maxPages && ++seen >= options.maxPages) break; } while (cursor); }