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