← 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/** 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 }
}