import type { Client } from './client.js' import type { SeamHttpRequestOptions } from './options.js' import { SeamHttpRequest } from './seam-http-request.js' interface SeamPaginatorParent { readonly client: Client readonly defaults: Required } declare const $brand: unique symbol export type SeamPageCursor = string & { [$brand]: 'SeamPageCursor' } interface Pagination { readonly hasNextPage: boolean readonly nextPageCursor: SeamPageCursor | null readonly nextPageUrl: string | null } export class SeamPaginator< const TResponse, const TResponseKey extends keyof TResponse, > implements AsyncIterable> { readonly #request: SeamHttpRequest readonly #parent: SeamPaginatorParent constructor( parent: SeamPaginatorParent, request: SeamHttpRequest, ) { if (request.responseKey == null) { throw new Error( `The ${request.pathname} endpoint does not support pagination`, ) } this.#parent = parent this.#request = request } async firstPage(): Promise< [EnsureReadonlyArray, Pagination] > { return await this.#fetch() } async nextPage( nextPageCursor: Pagination['nextPageCursor'], ): Promise<[EnsureReadonlyArray, Pagination]> { if (nextPageCursor == null) { throw new Error('Cannot get the next page with a null nextPageCursor') } return await this.#fetch(nextPageCursor) } async #fetch( nextPageCursor?: Pagination['nextPageCursor'], ): Promise<[EnsureReadonlyArray, Pagination]> { const responseKey = this.#request.responseKey if (responseKey == null) { throw new Error('Cannot paginate a response without a responseKey') } const request = new SeamHttpRequest(this.#parent, { pathname: this.#request.pathname, method: this.#request.method, responseKey, params: this.#request.params != null ? { ...this.#request.params, page_cursor: nextPageCursor } : undefined, body: this.#request.body != null ? { ...this.#request.body, page_cursor: nextPageCursor } : undefined, }) const response = await request.fetchResponse() const data = response[responseKey] const paginationData = response != null && typeof response === 'object' && 'pagination' in response ? (response.pagination as PaginationData) : null const pagination: Pagination = { hasNextPage: paginationData?.has_next_page ?? false, nextPageCursor: paginationData?.next_page_cursor ?? null, nextPageUrl: paginationData?.next_page_url ?? null, } if (!Array.isArray(data)) { throw new Error( `Expected an array response for ${String(responseKey)} but got ${String(typeof data)}`, ) } return [ data as EnsureReadonlyArray, pagination, ] as const } async flattenToArray(): Promise< EnsureReadonlyArray > { const items = [] as EnsureMutableArray let [current, pagination] = await this.firstPage() items.push(...current) while (pagination.hasNextPage) { ;[current, pagination] = await this.nextPage(pagination.nextPageCursor) items.push(...current) } return items as EnsureReadonlyArray } async *flatten(): AsyncGenerator< EnsureReadonlyArray > { let [current, pagination] = await this.firstPage() for (const item of current) { yield item } while (pagination.hasNextPage) { ;[current, pagination] = await this.nextPage(pagination.nextPageCursor) for (const item of current) { yield item } } } async *[Symbol.asyncIterator](): AsyncGenerator< EnsureReadonlyArray > { let [current, pagination] = await this.firstPage() yield current while (pagination.hasNextPage) { ;[current, pagination] = await this.nextPage(pagination.nextPageCursor) yield current } } } type EnsureReadonlyArray = T extends readonly any[] ? T : never type EnsureMutableArray = T extends any[] ? T : never interface PaginationData { has_next_page: boolean next_page_cursor: SeamPageCursor | null next_page_url: string | null }