This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / api / pagination.ts
3.7 kB 121 lines
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 /** stop after this many network round-trips. */ 34 maxPages?: number; 35} 36 37const requestPage = <TName extends PaginatedQuery>( 38 ctx: BobbinContext, 39 name: TName, 40 params: PageParams<TName> & { cursor?: string }, 41 init?: XrpcRequestInit 42): Promise<QueryOutput<TName>> => 43 ok( 44 ctx.xrpc.get(name, { 45 params, 46 signal: init?.signal, 47 headers: init?.headers 48 } as never) 49 ) as Promise<QueryOutput<TName>>; 50export const fetchPage = <TName extends PaginatedQuery>( 51 ctx: BobbinContext, 52 name: TName, 53 params: PageParams<TName> & { cursor?: string }, 54 init?: XrpcRequestInit 55): Promise<QueryOutput<TName>> => requestPage(ctx, name, params, init); 56 57export async function* pages<TName extends PaginatedQuery>( 58 ctx: BobbinContext, 59 name: TName, 60 params: PageParams<TName>, 61 options: PaginateOptions = {} 62): AsyncGenerator<QueryOutput<TName>, void, unknown> { 63 let cursor: string | undefined; 64 let seen = 0; 65 do { 66 const data = await requestPage( 67 ctx, 68 name, 69 { ...params, cursor } as PageParams<TName> & { cursor?: string }, 70 options 71 ); 72 yield data; 73 cursor = (data as CursorPage).cursor; 74 if (options.maxPages && ++seen >= options.maxPages) break; 75 } while (cursor); 76} 77 78export async function* items<TName extends PaginatedQuery>( 79 ctx: BobbinContext, 80 name: TName, 81 params: PageParams<TName>, 82 options: PaginateOptions = {} 83): AsyncGenerator<PageItem<TName>, void, unknown> { 84 for await (const data of pages(ctx, name, params, options)) { 85 for (const item of (data as CursorPage).items) yield item as PageItem<TName>; 86 } 87} 88 89export interface CollectOptions extends PaginateOptions { 90 /** stop once this many items are gathered. */ 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 108// cursor walker for schema-less list endpoints. 109export async function* paginateBy<T>( 110 load: (cursor: string | undefined) => Promise<{ items: readonly T[]; cursor?: string | null }>, 111 options: { maxPages?: number } = {} 112): AsyncGenerator<T, void, unknown> { 113 let cursor: string | undefined; 114 let seen = 0; 115 do { 116 const page = await load(cursor); 117 for (const item of page.items) yield item; 118 cursor = page.cursor ?? undefined; 119 if (options.maxPages && ++seen >= options.maxPages) break; 120 } while (cursor); 121}