This repository has no description
1import type { XRPCQueries } from "@atcute/lexicons/ambient";
2import type {
3 InferInput,
4 InferOutput,
5 ObjectSchema,
6 XRPCLexBodyParam
7} from "@atcute/lexicons/validations";
8import { ok, type BobbinContext, type XrpcRequestInit } from "./client";
9
10export type QueryOutput<TName extends keyof XRPCQueries> =
11 XRPCQueries[TName]["output"] extends XRPCLexBodyParam
12 ? InferOutput<XRPCQueries[TName]["output"]["schema"]>
13 : never;
14
15export type QueryParams<TName extends keyof XRPCQueries> =
16 XRPCQueries[TName]["params"] extends ObjectSchema
17 ? InferInput<XRPCQueries[TName]["params"]>
18 : Record<string, never>;
19
20type CursorPage = { items: readonly unknown[]; cursor?: string };
21
22// registered queries whose output is an { items, cursor? } page.
23export type PaginatedQuery = {
24 [K in keyof XRPCQueries]: QueryOutput<K> extends CursorPage ? K : never;
25}[keyof XRPCQueries];
26
27export type PageItem<TName extends PaginatedQuery> =
28 QueryOutput<TName> extends { items: readonly (infer I)[] } ? I : never;
29
30export type PageParams<TName extends PaginatedQuery> = Omit<QueryParams<TName>, "cursor">;
31
32export interface PaginateOptions extends XrpcRequestInit {
33 maxPages?: number;
34}
35
36const requestPage = <TName extends PaginatedQuery>(
37 ctx: BobbinContext,
38 name: TName,
39 params: PageParams<TName> & { cursor?: string },
40 init?: XrpcRequestInit
41): Promise<QueryOutput<TName>> =>
42 ok(
43 ctx.xrpc.get(name, {
44 params,
45 signal: init?.signal,
46 headers: init?.headers
47 } as never)
48 ) as Promise<QueryOutput<TName>>;
49export const fetchPage = <TName extends PaginatedQuery>(
50 ctx: BobbinContext,
51 name: TName,
52 params: PageParams<TName> & { cursor?: string },
53 init?: XrpcRequestInit
54): Promise<QueryOutput<TName>> => requestPage(ctx, name, params, init);
55
56export async function* pages<TName extends PaginatedQuery>(
57 ctx: BobbinContext,
58 name: TName,
59 params: PageParams<TName>,
60 options: PaginateOptions = {}
61): AsyncGenerator<QueryOutput<TName>, void, unknown> {
62 let cursor: string | undefined;
63 let seen = 0;
64 do {
65 const data = await requestPage(
66 ctx,
67 name,
68 { ...params, cursor } as PageParams<TName> & { cursor?: string },
69 options
70 );
71 yield data;
72 const next = (data as CursorPage).cursor;
73 if (next !== undefined && next === cursor) break;
74 cursor = next;
75 if (options.maxPages && ++seen >= options.maxPages) break;
76 } while (cursor);
77}
78
79export async function* items<TName extends PaginatedQuery>(
80 ctx: BobbinContext,
81 name: TName,
82 params: PageParams<TName>,
83 options: PaginateOptions = {}
84): AsyncGenerator<PageItem<TName>, void, unknown> {
85 for await (const data of pages(ctx, name, params, options)) {
86 for (const item of (data as CursorPage).items) yield item as PageItem<TName>;
87 }
88}
89
90export interface CollectOptions extends PaginateOptions {
91 max?: number;
92}
93
94export const collect = async <TName extends PaginatedQuery>(
95 ctx: BobbinContext,
96 name: TName,
97 params: PageParams<TName>,
98 options: CollectOptions = {}
99): Promise<PageItem<TName>[]> => {
100 const out: PageItem<TName>[] = [];
101 for await (const item of items(ctx, name, params, options)) {
102 out.push(item);
103 if (options.max && out.length >= options.max) break;
104 }
105 return out;
106};
107
108export async function* paginateBy<T>(
109 load: (cursor: string | undefined) => Promise<{ items: readonly T[]; cursor?: string | null }>,
110 options: { maxPages?: number } = {}
111): AsyncGenerator<T, void, unknown> {
112 let cursor: string | undefined;
113 let seen = 0;
114 do {
115 const page = await load(cursor);
116 for (const item of page.items) yield item;
117 const next = page.cursor ?? undefined;
118 if (next !== undefined && next === cursor) break;
119 cursor = next;
120 if (options.maxPages && ++seen >= options.maxPages) break;
121 } while (cursor);
122}