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 { /** stop after this many network round-trips. */ 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; cursor = (data as CursorPage).cursor; 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 { /** stop once this many items are gathered. */ 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; }; // cursor walker for schema-less list endpoints. 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; cursor = page.cursor ?? undefined; if (options.maxPages && ++seen >= options.maxPages) break; } while (cursor); }