infinity-fetch - v1.4.0
    Preparing search index...

    infinity-fetch - v1.4.0

    infinity-fetch 🚀

    npm version npm downloads npm bundle size License: MIT TypeScript CI Release Docs Changelog Issues Last commit

    Configurable recursive fetch for paginated APIs. Works in Node.js and browsers.

    Automatically re-invokes a fetch function across pages until a stop condition is met — accumulating all results into a single array. Zero dependencies.


                        ┌─────────────────────────────────────────────────────┐
    infinityFetch
    └────────────────────────┬────────────────────────────┘


    ┌────────────────────────────────────────┐
    fetcher(params) │
    └────────────────┬───────────────────────┘

    ┌────────────────▼───────────────────────┐
    Response
    └──────┬─────────────────────┬───────────┘
    │ │
    isLastPage? no
    │ │
    yes getNextParams()
    │ │
    │ ▼
    │ ┌────────────────────────┐
    │ │ fetcher(nextParams) │ ← repeats
    │ └────────────────────────┘


    ┌──────────────────────────────────────┐
    return { items[], pages: number } │
    itemsall pages accumulated
    pagestotal iterations done
    └──────────────────────────────────────┘

    Three convenience wrappers sit on top of infinityFetch:

    Helper Pagination style
    pagedFetch Offset-based — { start, limit, isLastPage, nextPageStart }
    cursorFetch Cursor/token-based — getCursor returns the next cursor or null
    infinityFetch Generic — you supply isLastPage and getNextParams

    npm install infinity-fetch
    

    If your API returns { values, isLastPage, nextPageStart, size, limit, start }, use the built-in helper:

    import { pagedFetch } from 'infinity-fetch';

    const { items, pages } = await pagedFetch({
    fetcher: (params) => api.project('my-project').repo('my-repo').commits(params),
    limit: 100, // items per page, defaults to 100
    });

    console.log(`${items.length} commits fetched across ${pages} pages`);

    With loading state, progress tracking, and a delay between pages:

    const { items, pages } = await pagedFetch({
    fetcher: (params) => api.project('my-project').repo('my-repo').commits(params),
    limit: 100,
    maxPages: 20,
    delay: 200,
    onStart: () => setLoading(true),
    onEnd: () => setLoading(false),
    onPage: (pageItems, _response, pageIndex) => {
    console.log(`Page ${pageIndex + 1}: ${pageItems.length} commits`);
    },
    });

    Use this when your API returns a cursor (or token) to request the next page, and null/undefined when there are no more pages:

    import { cursorFetch } from 'infinity-fetch';

    const { items, pages } = await cursorFetch({
    fetcher: ({ cursor }) => github.issues({ cursor, perPage: 50 }),
    getCursor: (r) => r.pageInfo.endCursor ?? null,
    getItems: (r) => r.data,
    });

    With full options:

    const { items, pages } = await cursorFetch({
    fetcher: ({ cursor }) => stripe.charges.list({ starting_after: cursor ?? undefined }),
    getCursor: (r) => r.has_more ? r.data.at(-1)?.id ?? null : null,
    getItems: (r) => r.data,
    maxPages: 50,
    delay: 100,
    onEnd: ({ items, pages }) => console.log(`${items.length} charges in ${pages} pages`),
    retry: { maxRetries: 2 },
    });

    Use this when your API has a different response shape:

    import { infinityFetch } from 'infinity-fetch';

    const { items, pages } = await infinityFetch({
    fetcher: (params) => github.issues.list(params),
    initialParams: { page: 1, per_page: 50 },
    isLastPage: (response) => response.data.length < 50,
    getNextParams: (_response, currentParams) => ({
    ...currentParams,
    page: currentParams.page + 1,
    }),
    getItems: (response) => response.data,
    maxPages: 100,
    delay: 200,
    retry: {
    maxRetries: 3,
    delay: (attempt) => attempt * 500,
    retryWhen: (error) => error instanceof Response && error.status >= 500,
    },
    onStart: () => setLoading(true),
    onEnd: ({ items, pages }) => {
    setLoading(false);
    console.log(`Done: ${items.length} items across ${pages} pages`);
    },
    onPage: (pageItems, _response, pageIndex) => {
    console.log(`Page ${pageIndex + 1}: ${pageItems.length} items`);
    },
    });

    All three helpers accept a signal option. When the signal fires, pagination stops immediately and returns whatever items were collected up to that point — no error is thrown.

    const controller = new AbortController();

    setTimeout(() => controller.abort(), 3000); // cancel after 3 seconds

    const { items, pages, aborted } = await cursorFetch({
    fetcher: ({ cursor }) => api.items({ cursor }),
    getCursor: (r) => r.nextCursor ?? null,
    getItems: (r) => r.data,
    signal: controller.signal,
    });

    if (aborted) {
    console.log(`Cancelled — got ${items.length} items across ${pages} pages`);
    }

    aborted is true in the result only when the signal fired. When pagination completes normally, the field is absent.


    When a fetch fails (after exhausting retries), infinityFetch throws an InfinityFetchError with full context about where the failure occurred:

    import { infinityFetch, InfinityFetchError } from 'infinity-fetch';

    try {
    const { items } = await infinityFetch({ /* ... */ });
    } catch (error) {
    if (error instanceof InfinityFetchError) {
    console.error(`Failed on page ${error.pageIndex}`);
    console.error(`Params at failure:`, error.params);
    console.error(`Items collected before failure:`, error.itemsSoFar);
    console.error(`Root cause:`, error.cause);
    }
    }
    Property Type Description
    pageIndex number Zero-based index of the page that failed
    params TParams Parameters that were passed to the fetcher
    itemsSoFar TItem[] All items collected from pages before the failure
    cause unknown The original error thrown by the fetcher
    message string "infinity-fetch failed on page N: <cause message>"

    Option Type Default Description
    fetcher (params: PagedParams) => Promise<PagedResponse<TItem>> required Function that fetches one page
    limit number 100 Items per page
    maxPages number Infinity Maximum pages to fetch (safety limit)
    delay number Milliseconds to wait between each page fetch
    retry InfinityFetchRetryConfig Retry failed page fetches
    signal AbortSignal Cancel pagination and return partial results
    onStart () => void Called once before the first fetch
    onEnd (result: InfinityFetchResult<TItem>) => void Called once after all pages are done, or when cancelled — receives { items, pages, aborted: true } on cancellation
    onPage (items, response, pageIndex) => void Called after each individual page

    PagedParams

    { start: number; limit: number }
    

    PagedResponse<TItem> — expected response shape:

    {
    values: TItem[];
    isLastPage: boolean;
    nextPageStart?: number;
    size: number;
    limit: number;
    start: number;
    }

    Returns: Promise<InfinityFetchResult<TItem>>

    Throws: InfinityFetchError on fetch failure. Also throws if a non-final page response is missing nextPageStart.


    Option Type Default Description
    fetcher (params: CursorParams) => Promise<TResponse> required Function that fetches one page
    getCursor (response: TResponse) => string | null | undefined required Returns the next cursor, or null/undefined on the last page
    getItems (response: TResponse) => TItem[] required Extracts items from a response
    maxPages number Infinity Maximum pages to fetch (safety limit)
    delay number Milliseconds to wait between each page fetch
    retry InfinityFetchRetryConfig Retry failed page fetches
    signal AbortSignal Cancel pagination and return partial results
    onStart () => void Called once before the first fetch
    onEnd (result: InfinityFetchResult<TItem>) => void Called once after all pages are done, or when cancelled — receives { items, pages, aborted: true } on cancellation
    onPage (items, response, pageIndex) => void Called after each individual page

    CursorParams

    { cursor: string | null }  // null on the first request (no prior cursor)
    

    Returns: Promise<InfinityFetchResult<TItem>>

    Throws: InfinityFetchError on fetch failure (after retries are exhausted).


    Option Type Default Description
    fetcher (params: TParams) => Promise<TResponse> required Function that fetches one page
    initialParams TParams required Parameters for the first request
    isLastPage (response: TResponse) => boolean required Returns true to stop iteration
    getNextParams (response: TResponse, currentParams: TParams) => TParams required Computes params for the next page
    getItems (response: TResponse) => TItem[] required Extracts items from a response
    maxPages number Infinity Maximum pages to fetch (safety limit)
    delay number Milliseconds to wait between each page fetch
    retry InfinityFetchRetryConfig Retry failed page fetches
    signal AbortSignal Cancel pagination and return partial results
    onStart () => void Called once before the first fetch
    onEnd (result: InfinityFetchResult<TItem>) => void Called once after all pages are done, or when cancelled — receives { items, pages, aborted: true } on cancellation
    onPage (items, response, pageIndex) => void Called after each individual page

    Returns: Promise<InfinityFetchResult<TItem>>

    Throws: InfinityFetchError on fetch failure (after retries are exhausted).


    type InfinityFetchResult<TItem> = {
    items: TItem[]; // all items collected across every page
    pages: number; // total number of pages fetched
    aborted?: true; // present only when an AbortSignal fired
    };

    InfinityFetchRetryConfig

    Field Type Default Description
    maxRetries number 0 Extra attempts per page. With 0, any failure throws immediately. Total attempts = maxRetries + 1.
    delay number | (attempt, error) => number Wait between retries of the same page (attempt is 1-based). Different from config.delay, which separates consecutive pages.
    retryWhen (error, attempt) => boolean | Promise<boolean> retry all errors If it returns false, the error is thrown immediately without exhausting maxRetries. attempt is 1-based.
    class InfinityFetchError<TParams, TItem> extends Error {
    readonly pageIndex: number; // zero-based index of the failed page
    readonly params: TParams; // params passed to the fetcher
    readonly itemsSoFar: TItem[]; // items collected before the failure
    readonly cause: unknown; // original error
    }

    All public types are available as named imports:

    import type {
    CursorFetchConfig,
    CursorParams,
    InfinityFetchConfig,
    InfinityFetchResult,
    InfinityFetchRetryConfig,
    PagedFetchConfig,
    PagedParams,
    PagedResponse,
    } from 'infinity-fetch';

    Environment Support
    Node.js 18+
    Node.js 20+
    Modern browsers
    Deno / Bun
    ESM
    TypeScript ✅ (types included)

    Commits must follow the Conventional Commits spec — this drives automatic versioning and changelog generation via semantic-release.

    Commit prefix Triggers
    fix: Patch release (0.0.x)
    feat: Minor release (0.x.0)
    feat!: / BREAKING CHANGE: Major release (x.0.0)
    chore:, docs:, test: No release
    git commit -m "feat: add onPage callback to pagedFetch"
    git commit -m "fix: handle missing nextPageStart gracefully"
    git commit -m "feat!: rename items field to data"

    See CHANGELOG.md for the full release history.


    MIT