Skip to content
← Public packages

@charlesmatte/granola

Search and read Granola meeting notes, summaries and transcripts from any agent; manage webhooks and legal holds safely.

src/lib/paginate.ts

38 lines · 1.8 KB · TypeScript
Raw
/** Granola list pages: `{ <items>: [...], hasMore: boolean, cursor: string | null }`. */
export type Page<T> = { items: Array<T>; hasMore: boolean; cursor: string | null }

export type PaginateResult<T> = { items: Array<T>; hasMore: boolean; cursor: string | null; pages: number }

/**
 * Collect up to `limit` items. With `paginate: false` only one page is fetched. The API caps
 * page_size (30 for most lists, 100 for transcripts), so pages are requested at min(remaining, max).
 * Paging follows `hasMore` + `cursor` only (a page may hold fewer items and still not be the last).
 * Items beyond `limit` on the last page are dropped, and `hasMore` / `cursor` then point at the
 * page after the one that was cut (the API has no item offset), so `truncated` is reported.
 */
export async function paginate<T>(
	fetchPage: (cursor: string | undefined, pageSize: number) => Promise<Page<T>>,
	opts: { limit: number; maxPageSize: number; cursor?: string; paginate: boolean },
): Promise<PaginateResult<T> & { truncated: boolean }> {
	const items: Array<T> = []
	let cursor: string | undefined = opts.cursor
	let hasMore = false
	let nextCursor: string | null = null
	let pages = 0
	let truncated = false
	for (;;) {
		const remaining = opts.limit - items.length
		if (remaining <= 0) break
		const page = await fetchPage(cursor, Math.min(remaining, opts.maxPageSize))
		pages++
		const take = page.items.slice(0, remaining)
		if (take.length < page.items.length) truncated = true
		items.push(...take)
		hasMore = page.hasMore
		nextCursor = page.cursor ?? null
		if (!opts.paginate || !page.hasMore || !page.cursor) break
		if (page.cursor === cursor) break // defensive: never loop on a stuck cursor
		cursor = page.cursor
	}
	return { items, hasMore, cursor: hasMore ? nextCursor : null, pages, truncated }
}