//#region src/utils/paginated-lists.d.ts type QueryOptions = { filter: Filter; orderBy: OrderBy; limit: number; /** * Whether the limit should be treated as an exact value, or an approximate value. * * If set to 'exact', less items will only be returned if the list item is the first or last item. * * If set to 'at-least' or 'approximate', the implementation may decide to return more items than the limit requested if doing so comes at no (or negligible) extra cost. * * If set to 'at-most' or 'approximate', the implementation may decide to return less items than the limit requested if requesting more items would come at a non-negligible extra cost. In this case, if limit > 0, the implementation must still make progress towards the end of the list and the returned cursor must be different from the one passed in. * * Defaults to 'exact'. */ limitPrecision: 'exact' | 'at-least' | 'at-most' | 'approximate'; } & ([Type] extends [never] ? unknown : [Type] extends ['next'] ? { after: Cursor; } : [Type] extends ['prev'] ? { before: Cursor; } : { cursor: Cursor; }); type ImplQueryOptions = QueryOptions & { limitPrecision: 'approximate'; }; type QueryResult = { items: { item: Item; prevCursor: Cursor; nextCursor: Cursor; }[]; isFirst: boolean; isLast: boolean; cursor: Cursor; }; type ImplQueryResult = { items: { item: Item; prevCursor: Cursor; nextCursor: Cursor; }[]; isFirst: boolean; isLast: boolean; cursor: Cursor; }; /** * Abstract base class for cursor-based pagination over any ordered data source. * * Subclasses implement `_nextOrPrev` to fetch items in one direction. This class handles * limit enforcement, sorting validation, and provides `map`, `filter`, `flatMap`, and `merge` utilities. * * @template Item - The type of items in the list * @template Cursor - A string-based cursor type for position tracking. Cursors are always between two items in the list. Note that cursors may not be stable if the filter or orderBy changes. * @template Filter - Query filter type * @template OrderBy - Sort order specification type * * @example * ```ts * // Basic usage: paginate through users * const users = new MyUserList(); * const first10 = await users.next({ after: users.getFirstCursor(), limit: 10, filter: {}, orderBy: 'name', limitPrecision: 'exact' }); * // first10 = { items: [...], isFirst: true, isLast: false, cursor: "cursor-after-item-10" } * * const next10 = await users.next({ after: first10.cursor, limit: 10, filter: {}, orderBy: 'name', limitPrecision: 'exact' }); * // Continues from where we left off * ``` */ declare abstract class PaginatedList { protected abstract _getFirstCursor(): Cursor; protected abstract _getLastCursor(): Cursor; protected abstract _compare(orderBy: OrderBy, a: Item, b: Item): number; protected abstract _nextOrPrev(type: 'next' | 'prev', options: ImplQueryOptions<'next' | 'prev', Cursor, Filter, OrderBy>): Promise>; /** Returns the cursor pointing to the start of the list (before any items). */ getFirstCursor(): Cursor; /** Returns the cursor pointing to the end of the list (after all items). */ getLastCursor(): Cursor; /** Compares two items according to the given orderBy. Returns negative if a < b, 0 if equal, positive if a > b. */ compare(orderBy: OrderBy, a: Item, b: Item): number; /** * Fetches items moving forward ('next') or backward ('prev') from the given cursor. * * Respects `limitPrecision`: 'exact' guarantees the exact limit, 'at-least'/'at-most'/'approximate' * allow flexibility for performance. Returns items, boundary flags, and a new cursor. * * @example * ```ts * // Get 5 items after the start * const result = await list.nextOrPrev('next', { cursor: list.getFirstCursor(), limit: 5, filter: {}, orderBy: 'asc', limitPrecision: 'exact' }); * // result.items.length === 5 (or less if list has fewer items) * // result.isFirst === true (started at first cursor) * // result.isLast === true if we got all remaining items * * // Continue from where we left off * const more = await list.nextOrPrev('next', { cursor: result.cursor, limit: 5, ... }); * ``` */ nextOrPrev(type: 'next' | 'prev', options: QueryOptions<'next' | 'prev', Cursor, Filter, OrderBy>): Promise>; /** Fetches items after the given cursor (forward pagination). */ next({ after, ...rest }: QueryOptions<'next', Cursor, Filter, OrderBy>): Promise>; /** Fetches items before the given cursor (backward pagination). */ prev({ before, ...rest }: QueryOptions<'prev', Cursor, Filter, OrderBy>): Promise>; /** * Transforms this list by mapping each item to zero or more new items. * * Note that the sort order must be preserved after the operation; the flat-mapped list will not be sorted automatically. * * @param itemMapper - Maps each item (with its cursor) to an array of new items * @param compare - Comparison function for the new item type * @param newCursorFromOldCursor/oldCursorFromNewCursor - Cursor conversion functions * @param estimateItemsToFetch - Estimates how many source items to fetch for a given limit * * @example * ```ts * // Expand orders into line items (1 order -> N line items) * const lineItems = ordersList.flatMap({ * itemMapper: ({ item: order }) => order.lineItems.map((li, i) => ({ item: li, prevCursor: `${order.id}-${i}`, nextCursor: `${order.id}-${i + 1}` })), * compare: (_, a, b) => a.createdAt - b.createdAt, * estimateItemsToFetch: ({ limit }) => Math.ceil(limit / 3), // avg 3 items per order * // ... cursor converters * }); * ``` */ flatMap(options: { itemMapper: (itemEntry: { item: Item; prevCursor: Cursor; nextCursor: Cursor; }, filter: Filter2, orderBy: OrderBy2) => { item: Item2; prevCursor: Cursor2; nextCursor: Cursor2; }[]; compare: (orderBy: OrderBy2, a: Item2, b: Item2) => number; newCursorFromOldCursor: (cursor: Cursor) => Cursor2; oldCursorFromNewCursor: (cursor: Cursor2) => Cursor; oldFilterFromNewFilter: (filter: Filter2) => Filter; oldOrderByFromNewOrderBy: (orderBy: OrderBy2) => OrderBy; estimateItemsToFetch: (options: { filter: Filter2; orderBy: OrderBy2; limit: number; }) => number; }): PaginatedList; /** * Transforms each item in the list. Requires a reverse mapper for comparison delegation. * * @param itemMapper - Transforms each item * @param oldItemFromNewItem - Reverse-maps new items back to old items (for comparison) * * @example * ```ts * // Convert User objects to UserDTO * const userDtos = usersList.map({ * itemMapper: (user) => ({ id: user.id, displayName: user.name }), * oldItemFromNewItem: (dto) => fullUsers.get(dto.id)!, // for comparison * oldFilterFromNewFilter: (f) => f, * oldOrderByFromNewOrderBy: (o) => o, * }); * ``` */ map(options: { itemMapper: (item: Item) => Item2; oldItemFromNewItem: (item: Item2) => Item; oldFilterFromNewFilter: (filter: Filter2) => Filter; oldOrderByFromNewOrderBy: (orderBy: OrderBy2) => OrderBy; }): PaginatedList; /** * Filters items in the list. Requires an estimate function since filtering may reduce output. * * @param filter - Predicate to include/exclude items * @param estimateItemsToFetch - Estimates how many source items to fetch (accounts for filter selectivity) * * @example * ```ts * // Filter to only active users * const activeUsers = usersList.filter({ * filter: (user, filterOpts) => user.isActive && user.role === filterOpts.role, * oldFilterFromNewFilter: (f) => ({}), // original list has no filter * estimateItemsToFetch: ({ limit }) => limit * 2, // expect ~50% active * }); * ``` */ filter(options: { filter: (item: Item, filter: Filter2) => boolean; oldFilterFromNewFilter: (filter: Filter2) => Filter; estimateItemsToFetch: (options: { filter: Filter2; orderBy: OrderBy; limit: number; }) => number; }): PaginatedList; /** * Adds an additional filter constraint while preserving the original filter type. * Shorthand for `filter()` that intersects Filter with AddedFilter. * * @example * ```ts * // Add a "verified" filter on top of existing filters * const verifiedUsers = usersList.addFilter({ * filter: (user, f) => user.emailVerified, * estimateItemsToFetch: ({ limit }) => limit * 2, // ~50% are verified * }); * // verifiedUsers filter type is Filter * ``` */ addFilter(options: { filter: (item: Item, filter: Filter & AddedFilter) => boolean; estimateItemsToFetch: (options: { filter: Filter & AddedFilter; orderBy: OrderBy; limit: number; }) => number; }): PaginatedList; /** * Merges multiple paginated lists into one, interleaving items by sort order. * All lists must use the same compare function. * * The merged cursor is a JSON-encoded array of individual list cursors. * * @example * ```ts * // Merge users from multiple sources into a unified feed * const allUsers = PaginatedList.merge(internalUsers, externalUsers, partnerUsers); * const page = await allUsers.next({ after: allUsers.getFirstCursor(), limit: 20, ... }); * // page.items contains interleaved items from all sources, sorted by orderBy * ``` */ static merge(...lists: PaginatedList[]): PaginatedList; /** * Returns an empty paginated list that always returns no items. * * @example * ```ts * const empty = PaginatedList.empty(); * const result = await empty.next({ after: empty.getFirstCursor(), limit: 10, ... }); * // result = { items: [], isFirst: true, isLast: true, cursor: "first" } * ``` */ static empty(): { _getFirstCursor(): "first"; _getLastCursor(): "last"; _compare(orderBy: any, a: any, b: any): number; _nextOrPrev(type: "next" | "prev", options: ImplQueryOptions<"next" | "prev", string, any, any>): Promise<{ items: never[]; isFirst: boolean; isLast: boolean; cursor: "first"; }>; /** Returns the cursor pointing to the start of the list (before any items). */ getFirstCursor(): "first" | "last"; /** Returns the cursor pointing to the end of the list (after all items). */ getLastCursor(): "first" | "last"; /** Compares two items according to the given orderBy. Returns negative if a < b, 0 if equal, positive if a > b. */ compare(orderBy: any, a: never, b: never): number; /** * Fetches items moving forward ('next') or backward ('prev') from the given cursor. * * Respects `limitPrecision`: 'exact' guarantees the exact limit, 'at-least'/'at-most'/'approximate' * allow flexibility for performance. Returns items, boundary flags, and a new cursor. * * @example * ```ts * // Get 5 items after the start * const result = await list.nextOrPrev('next', { cursor: list.getFirstCursor(), limit: 5, filter: {}, orderBy: 'asc', limitPrecision: 'exact' }); * // result.items.length === 5 (or less if list has fewer items) * // result.isFirst === true (started at first cursor) * // result.isLast === true if we got all remaining items * * // Continue from where we left off * const more = await list.nextOrPrev('next', { cursor: result.cursor, limit: 5, ... }); * ``` */ nextOrPrev(type: "next" | "prev", options: QueryOptions<"next" | "prev", "first" | "last", any, any>): Promise>; /** Fetches items after the given cursor (forward pagination). */ next({ after, ...rest }: QueryOptions<"next", "first" | "last", any, any>): Promise>; /** Fetches items before the given cursor (backward pagination). */ prev({ before, ...rest }: QueryOptions<"prev", "first" | "last", any, any>): Promise>; /** * Transforms this list by mapping each item to zero or more new items. * * Note that the sort order must be preserved after the operation; the flat-mapped list will not be sorted automatically. * * @param itemMapper - Maps each item (with its cursor) to an array of new items * @param compare - Comparison function for the new item type * @param newCursorFromOldCursor/oldCursorFromNewCursor - Cursor conversion functions * @param estimateItemsToFetch - Estimates how many source items to fetch for a given limit * * @example * ```ts * // Expand orders into line items (1 order -> N line items) * const lineItems = ordersList.flatMap({ * itemMapper: ({ item: order }) => order.lineItems.map((li, i) => ({ item: li, prevCursor: `${order.id}-${i}`, nextCursor: `${order.id}-${i + 1}` })), * compare: (_, a, b) => a.createdAt - b.createdAt, * estimateItemsToFetch: ({ limit }) => Math.ceil(limit / 3), // avg 3 items per order * // ... cursor converters * }); * ``` */ flatMap(options: { itemMapper: (itemEntry: { item: never; prevCursor: "first" | "last"; nextCursor: "first" | "last"; }, filter: Filter2, orderBy: OrderBy2) => { item: Item2; prevCursor: Cursor2; nextCursor: Cursor2; }[]; compare: (orderBy: OrderBy2, a: Item2, b: Item2) => number; newCursorFromOldCursor: (cursor: "first" | "last") => Cursor2; oldCursorFromNewCursor: (cursor: Cursor2) => "first" | "last"; oldFilterFromNewFilter: (filter: Filter2) => any; oldOrderByFromNewOrderBy: (orderBy: OrderBy2) => any; estimateItemsToFetch: (options: { filter: Filter2; orderBy: OrderBy2; limit: number; }) => number; }): PaginatedList; /** * Transforms each item in the list. Requires a reverse mapper for comparison delegation. * * @param itemMapper - Transforms each item * @param oldItemFromNewItem - Reverse-maps new items back to old items (for comparison) * * @example * ```ts * // Convert User objects to UserDTO * const userDtos = usersList.map({ * itemMapper: (user) => ({ id: user.id, displayName: user.name }), * oldItemFromNewItem: (dto) => fullUsers.get(dto.id)!, // for comparison * oldFilterFromNewFilter: (f) => f, * oldOrderByFromNewOrderBy: (o) => o, * }); * ``` */ map(options: { itemMapper: (item: never) => Item2; oldItemFromNewItem: (item: Item2) => never; oldFilterFromNewFilter: (filter: Filter2) => any; oldOrderByFromNewOrderBy: (orderBy: OrderBy2) => any; }): PaginatedList; /** * Filters items in the list. Requires an estimate function since filtering may reduce output. * * @param filter - Predicate to include/exclude items * @param estimateItemsToFetch - Estimates how many source items to fetch (accounts for filter selectivity) * * @example * ```ts * // Filter to only active users * const activeUsers = usersList.filter({ * filter: (user, filterOpts) => user.isActive && user.role === filterOpts.role, * oldFilterFromNewFilter: (f) => ({}), // original list has no filter * estimateItemsToFetch: ({ limit }) => limit * 2, // expect ~50% active * }); * ``` */ filter(options: { filter: (item: never, filter: Filter2) => boolean; oldFilterFromNewFilter: (filter: Filter2) => any; estimateItemsToFetch: (options: { filter: Filter2; orderBy: any; limit: number; }) => number; }): PaginatedList; /** * Adds an additional filter constraint while preserving the original filter type. * Shorthand for `filter()` that intersects Filter with AddedFilter. * * @example * ```ts * // Add a "verified" filter on top of existing filters * const verifiedUsers = usersList.addFilter({ * filter: (user, f) => user.emailVerified, * estimateItemsToFetch: ({ limit }) => limit * 2, // ~50% are verified * }); * // verifiedUsers filter type is Filter * ``` */ addFilter(options: { filter: (item: never, filter: any) => boolean; estimateItemsToFetch: (options: { filter: any; orderBy: any; limit: number; }) => number; }): PaginatedList; }; } /** * A simple in-memory paginated list backed by an array. * * Filter is a predicate function, OrderBy is a comparator function. * Cursors are in the format "before-{index}" representing the position before that index. * * Note: This implementation re-filters and re-sorts the entire array on each query, * so it's only suitable for small datasets. * * @example * ```ts * const numbers = new ArrayPaginatedList([5, 2, 8, 1, 9, 3]); * const page = await numbers.next({ * after: "before-0", * limit: 3, * filter: (n) => n > 2, * orderBy: (a, b) => a - b, * limitPrecision: 'exact', * }); * // page.items = [{ item: 3, prevCursor: "before-0", nextCursor: "before-1" }, { item: 5, prevCursor: "before-1", nextCursor: "before-2" }, ...] * ``` */ declare class ArrayPaginatedList extends PaginatedList boolean, (a: Item, b: Item) => number> { private readonly array; constructor(array: Item[]); _getFirstCursor(): "before-0"; _getLastCursor(): `before-${number}`; _compare(orderBy: (a: Item, b: Item) => number, a: Item, b: Item): number; _nextOrPrev(type: 'next' | 'prev', options: ImplQueryOptions<'next' | 'prev', `before-${number}`, (item: Item) => boolean, (a: Item, b: Item) => number>): Promise<{ items: { item: Item; prevCursor: `before-${number}`; nextCursor: `before-${number}`; }[]; isFirst: boolean; isLast: boolean; cursor: `before-${number}`; }>; } //#endregion export { ArrayPaginatedList, PaginatedList }; //# sourceMappingURL=paginated-lists.d.ts.map