Iterate paginated API results with async generators

TypeScript
// Walk cursor-paginated pages with for await, stopping on a repeated cursor.
type Page<T> = { items: T[]; nextCursor: string | null };

async function* paginate<T>(
  loadPage: (cursor: string | null) => Promise<Page<T>>,
): AsyncGenerator<T, void, undefined> {
  let cursor: string | null = null;
  const seen = new Set<string>();

  for (;;) {
    const page = await loadPage(cursor);
    yield* page.items;
    if (!page.nextCursor) return;
    if (seen.has(page.nextCursor)) {
      throw new Error(`pagination loop detected at cursor "${page.nextCursor}"`);
    }
    seen.add(page.nextCursor);
    cursor = page.nextCursor;
  }
}

async function loadPage(cursor: string | null): Promise<Page<{ id: number }>> {
  const start = cursor ? Number(cursor) : 0;
  const items = [start, start + 1].map((id) => ({ id }));
  const nextCursor = start + 2 < 6 ? String(start + 2) : null;
  return { items, nextCursor };
}

(async () => {
  const ids: number[] = [];
  for await (const item of paginate(loadPage)) ids.push(item.id);
  console.log(ids); // [0, 1, 2, 3, 4, 5]
})();