Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107import { describe, expect, it, vi, type Mock } from 'vitest';import { createBobbinClient, type BobbinContext } from './client';import { collect, pages, paginateBy } from './pagination';
const NAME = 'sh.tangled.feed.listStars';const PARAMS = { subject: 'did:plc:x' } as const;
interface Page { items: readonly { uri: string }[]; cursor?: string;}
const pageResponse = (page: Page): Response => new Response(JSON.stringify(page), { status: 200, headers: { 'content-type': 'application/json' } });
const makeCtx = (fetchMock: typeof globalThis.fetch): BobbinContext => createBobbinClient({ serviceUrl: 'https://bobbin.test', fetch: fetchMock });
const cursorOf = (mock: Mock<typeof globalThis.fetch>, n: number): string | null => new URL(String(mock.mock.calls[n][0])).searchParams.get('cursor');
const threePageFetch = (): Mock<typeof globalThis.fetch> => vi .fn<typeof globalThis.fetch>() .mockResolvedValueOnce(pageResponse({ items: [{ uri: 'a' }, { uri: 'b' }], cursor: 'c1' })) .mockResolvedValueOnce(pageResponse({ items: [{ uri: 'c' }], cursor: 'c2' })) .mockResolvedValueOnce(pageResponse({ items: [{ uri: 'd' }] }));
describe('pages / items / collect follow the cursor', () => { it('pages() yields every page then stops when a page omits the cursor', async () => { const fetchMock = threePageFetch(); const seen: Page[] = []; for await (const p of pages(makeCtx(fetchMock), NAME, PARAMS)) seen.push(p as Page); expect(fetchMock).toHaveBeenCalledTimes(3); expect(seen.map((p) => p.cursor)).toEqual(['c1', 'c2', undefined]); expect(cursorOf(fetchMock, 0)).toBeNull(); expect(cursorOf(fetchMock, 1)).toBe('c1'); expect(cursorOf(fetchMock, 2)).toBe('c2'); });});
describe('pagination caps', () => { it('maxPages caps the number of network round-trips even with unbounded cursors', async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockImplementation(async () => pageResponse({ items: [{ uri: 'x' }], cursor: 'always' })); const out = await collect(makeCtx(fetchMock), NAME, PARAMS, { maxPages: 2 }); expect(fetchMock).toHaveBeenCalledTimes(2); expect(out).toHaveLength(2); });
it('collect({ max }) caps items mid-page without fetching the next page', async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValueOnce( pageResponse({ items: [{ uri: 'a' }, { uri: 'b' }, { uri: 'c' }], cursor: 'c1' }) ); const out = await collect(makeCtx(fetchMock), NAME, PARAMS, { max: 2 }); expect(out).toEqual([{ uri: 'a' }, { uri: 'b' }]); expect(fetchMock).toHaveBeenCalledTimes(1); });});
describe('paginateBy', () => { it('treats cursor: null as the end of the stream', async () => { const load = vi.fn(async () => ({ items: [1, 2] as const, cursor: null })); const out: number[] = []; for await (const n of paginateBy(load)) out.push(n); expect(out).toEqual([1, 2]); expect(load).toHaveBeenCalledTimes(1); });
it('follows a string cursor and forwards it to the loader', async () => { const load = vi .fn< ( cursor: string | undefined ) => Promise<{ items: readonly number[]; cursor?: string | null }> >() .mockResolvedValueOnce({ items: [1], cursor: 'p2' }) .mockResolvedValueOnce({ items: [2], cursor: undefined }); const out: number[] = []; for await (const n of paginateBy(load)) out.push(n); expect(out).toEqual([1, 2]); expect(load).toHaveBeenCalledTimes(2); expect(load.mock.calls[0][0]).toBeUndefined(); expect(load.mock.calls[1][0]).toBe('p2'); });
it('maxPages caps loader invocations', async () => { const load = vi.fn(async () => ({ items: [0], cursor: 'always' })); const out: number[] = []; for await (const n of paginateBy(load, { maxPages: 3 })) out.push(n); expect(load).toHaveBeenCalledTimes(3); expect(out).toHaveLength(3); });});