import type { Client, CombinedError, OperationResult, TypedDocumentNode } from "urql"; /** Minimal shape of a Relay-style connection the exporter walks. */ export interface CursorConnection { pageInfo: { hasNextPage: boolean; endCursor?: string | null }; edges: readonly { node: Node }[]; } // Tailor caps a single page at 1000 rows, so that's the largest page we can ask // for. Callers may request a smaller page via the `pageSize` argument. Each page // retries on transient (network) errors; permanent GraphQL errors fail fast. // Shared by every list-view CSV export (issue #605). const PAGE_SIZE = 1000; const MAX_RETRIES = 3; // initial attempt + up to 3 retries const RETRY_BASE_MS = 300; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const isRetryable = (error: CombinedError): boolean => Boolean(error.networkError); /** * Fetch every row of a cursor-paginated query by paging through it until the * server reports `hasNextPage: false`. `variables` should carry the caller's * `order` + `query` (filter/sort); `first`/`after` are injected per page. * * A failed page re-fetches just that page (same cursor); exhausting the retries * throws so the caller can fail the whole export rather than emit a partial * result. Stops if the cursor stops advancing, so a misbehaving backend can't * spin this into an infinite loop. * * `pageSize` controls how many rows each request asks for (defaults to the 1000 * server cap; pass a smaller value to trade more round-trips for lighter pages). */ export async function fetchAllPages, Node>( client: Client, query: TypedDocumentNode, variables: Variables, selectConnection: (data: Result) => CursorConnection | null | undefined, pageSize: number = PAGE_SIZE, ): Promise { // Clamp to a positive integer within Tailor's 1000-row page cap so a bad // caller value can't produce a failing or surprising query. NaN / Infinity // fall back to the cap rather than passing through as `first`. const size = Number.isFinite(pageSize) ? Math.min(Math.max(1, Math.trunc(pageSize)), PAGE_SIZE) : PAGE_SIZE; const rows: Node[] = []; let after: string | null = null; let hasNextPage = true; while (hasNextPage) { let result: OperationResult; for (let attempt = 0; ; attempt++) { result = await client .query(query, { ...variables, first: size, after }, { requestPolicy: "network-only" }) .toPromise(); if (!result.error) break; if (attempt >= MAX_RETRIES || !isRetryable(result.error)) throw result.error; await sleep(RETRY_BASE_MS * 2 ** attempt + Math.random() * 100); } const connection = result.data ? selectConnection(result.data) : null; if (!connection) break; rows.push(...connection.edges.map((e) => e.node)); hasNextPage = connection.pageInfo.hasNextPage && Boolean(connection.pageInfo.endCursor); after = connection.pageInfo.endCursor ?? null; } return rows; }