Skip to main content
Glama
manager.ts7.66 kB
/** * @fileoverview Pagination manager for handling multi-page data fetching * This module provides utilities for orchestrating pagination across multiple pages. */ import { createLogger } from '../logging/logger.js'; import { PageInfo, PaginationParams, PaginatedResponse, MultiPageOptions, MultiPageResult, PaginatedResponseWithMetadata, PaginationMetadata, } from './types.js'; import { normalizePaginationParams } from './helpers.js'; const logger = createLogger('PaginationManager'); /** * Type for a function that fetches a page of data * @template T The type of items being fetched */ export type PageFetcher<T> = (cursor?: string, pageSize?: number) => Promise<PaginatedResponse<T>>; /** * Fetches multiple pages of data with configurable limits * @template T The type of items being fetched * @param fetcher Function that fetches a single page * @param options Configuration for multi-page fetching * @returns Aggregated results from all fetched pages */ export async function fetchMultiplePages<T>( fetcher: PageFetcher<T>, options: MultiPageOptions = {} ): Promise<MultiPageResult<T>> { const { maxPages = 10, pageSize = 50, fetchAll = false, onProgress } = options; const allItems: T[] = []; let pagesFetched = 0; let currentCursor: string | undefined; let hasMore = true; let totalCount: number | undefined; logger.debug('Starting multi-page fetch', { maxPages, pageSize, fetchAll, }); // Use a bounded loop to prevent infinite loop warnings const maxIterations = fetchAll ? Number.MAX_SAFE_INTEGER : maxPages; for (let iteration = 0; iteration < maxIterations && hasMore; iteration++) { try { // Fetch the next page const response = await fetcher(currentCursor, pageSize); // Add items to the collection allItems.push(...response.items); pagesFetched++; // Update pagination state hasMore = response.pageInfo.hasNextPage; currentCursor = response.pageInfo.endCursor; totalCount = response.totalCount; // Report progress if callback provided if (onProgress) { onProgress(pagesFetched, allItems.length); } logger.debug('Fetched page', { pageNumber: pagesFetched, itemsInPage: response.items.length, totalItemsSoFar: allItems.length, hasMore, }); // Break if no more pages if (!hasMore) { break; } } catch (error) { logger.error('Error fetching page', { pageNumber: pagesFetched + 1, error, }); // Ensure loop termination on error hasMore = false; throw error; } } logger.info('Multi-page fetch completed', { pagesFetched, totalItems: allItems.length, hasMore, }); return { items: allItems, pagesFetched, hasMore, ...(currentCursor && { lastCursor: currentCursor }), ...(totalCount && { totalCount }), }; } /** * Adds user-friendly pagination metadata to a standard response * @template T The type of items in the response * @param response Standard paginated response * @param pagesFetched Number of pages that were fetched (for multi-page operations) * @param limitReached Whether a max_pages limit was reached * @returns Response with additional metadata */ export function addPaginationMetadata<T>( response: PaginatedResponse<T>, pagesFetched = 1, limitReached = false ): PaginatedResponseWithMetadata<T> { const metadata: PaginationMetadata = { has_more_pages: response.pageInfo.hasNextPage, page_size: response.items.length, ...(response.pageInfo.endCursor && { next_cursor: response.pageInfo.endCursor }), ...(response.pageInfo.startCursor && { previous_cursor: response.pageInfo.startCursor }), ...(response.totalCount > 0 && { total_count: response.totalCount }), ...(pagesFetched > 1 && { pages_fetched: pagesFetched }), ...(limitReached && { limit_reached: limitReached }), }; return { ...response, pagination: metadata, }; } /** * Handles pagination parameters including page_size alias and max_pages * @param params Raw pagination parameters from user input * @returns Normalized parameters ready for API calls */ export function processPaginationParams(params: PaginationParams): { normalizedParams: PaginationParams; maxPages?: number; } { const { page_size, max_pages, ...restParams } = params; // Use page_size as an alias for first if provided if (page_size !== undefined && restParams.first === undefined) { restParams.first = page_size; } // Normalize the parameters const normalizedParams = normalizePaginationParams(restParams); return { normalizedParams, ...(max_pages !== undefined && { maxPages: max_pages }), }; } /** * Creates a pagination iterator for async iteration over pages * @template T The type of items being fetched * @param fetcher Function that fetches a single page * @param pageSize Size of each page * @returns Async iterator for pages */ export function createPaginationIterator<T>( fetcher: PageFetcher<T>, pageSize = 50 ): AsyncIterable<T[]> { return { [Symbol.asyncIterator](): AsyncIterator<T[]> { let currentCursor: string | undefined; let hasMore = true; return { async next(): Promise<IteratorResult<T[]>> { if (!hasMore) { return { done: true, value: undefined }; } try { const response = await fetcher(currentCursor, pageSize); hasMore = response.pageInfo.hasNextPage; currentCursor = response.pageInfo.endCursor; return { done: false, value: response.items, }; } catch (error) { logger.error('Error in pagination iterator', { error }); throw error; } }, }; }, }; } /** * Validates cursor format and checks for expiration * @param cursor The cursor to validate * @returns Whether the cursor is valid */ export function isValidCursor(cursor: string | undefined): boolean { if (cursor === undefined) { return true; // Undefined cursor is valid (starts from beginning) } // Basic validation - cursor should be a non-empty string return typeof cursor === 'string' && cursor.trim().length > 0; } /** * Merges multiple paginated responses into a single response * @template T The type of items being merged * @param responses Array of paginated responses to merge * @returns Merged paginated response */ export function mergeResponses<T>(responses: PaginatedResponse<T>[]): PaginatedResponse<T> { if (responses.length === 0) { return { items: [], pageInfo: { hasNextPage: false, hasPreviousPage: false, }, totalCount: 0, }; } const allItems = responses.flatMap((r) => r.items); const firstResponse = responses[0]; const lastResponse = responses[responses.length - 1]; if (!firstResponse || !lastResponse) { return { items: allItems, pageInfo: { hasNextPage: false, hasPreviousPage: false, }, totalCount: allItems.length, }; } const pageInfo: PageInfo = { hasNextPage: lastResponse.pageInfo.hasNextPage, hasPreviousPage: firstResponse.pageInfo.hasPreviousPage, ...(firstResponse.pageInfo.startCursor && { startCursor: firstResponse.pageInfo.startCursor, }), ...(lastResponse.pageInfo.endCursor && { endCursor: lastResponse.pageInfo.endCursor }), }; return { items: allItems, pageInfo, totalCount: lastResponse.totalCount || allItems.length, }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sapientpants/deepsource-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server