# Offset Pagination _(no submodules)_ ## Purpose The offset pagination module provides the computational building blocks for traditional page-number-based pagination. It handles request normalisation, metadata calculation, page-to-offset conversion, and in-memory array slicing. All functions are pure and synchronous except `estimateTotalCount`, which calls an adapter to sample the dataset. Offset pagination is the correct choice when total count is cheap to compute, pages are shallow (< a few thousand rows), or the UI must render absolute page numbers. For deep pagination on large datasets, prefer keyset or cursor pagination to avoid scanning skipped rows. ## Features - `calculateOffsetMeta` — compute full metadata (page, limit, total, totalPages, hasNext, hasPrevious). - `createOffsetResponse` — assemble a typed `OffsetPaginatedResponse` envelope. - `normalizeOffsetRequest` — clamp page/limit to configured defaults, default sortOrder. - `pageToOffset` — convert 1-based page number to a zero-based array/SQL offset. - `offsetToPage` — convert a zero-based offset back to a 1-based page number. - `getPageRange` — produce a windowed list of page numbers for UI navigation controls. - `paginateArray` — slice an in-memory array and return a full `OffsetPaginatedResponse`. - `estimateTotalCount` — sample-based approximate total count via adapter. ## Usage: Basic Example ```typescript import { paginateArray } from "@suaveplan/pagination/offset"; const items = Array.from({ length: 50 }, (_, i) => ({ id: i + 1 })); const page2 = paginateArray(items, 2, 10); // page2.data → items 11–20 // page2.meta.page → 2 // page2.meta.totalPages → 5 // page2.meta.hasNext → true // page2.meta.hasPrevious → true ``` ## Usage: Advanced Example ```typescript import { normalizeOffsetRequest, getPageRange, pageToOffset, offsetToPage, } from "@suaveplan/pagination/offset"; // Normalise untrusted input from a query string const req = normalizeOffsetRequest( { page: -5, limit: 9999 }, { maxLimit: 100, defaultLimit: 20 } ); // req.page → 1 (clamped), req.limit → 100 (clamped) // Render page buttons for a 300-item result set const pages = getPageRange(15, 30, 7); // → [12, 13, 14, 15, 16, 17, 18] // Convert offset 40 at limit 10 → page 5 const page = offsetToPage(40, 10); // → 5 const offset = pageToOffset(5, 10); // → 40 ``` ## API Reference ### `calculateOffsetMeta(total, page, limit): OffsetPaginationMeta` | Parameter | Type | Description | |-----------|----------|--------------------------------------| | `total` | `number` | Total item count across all pages | | `page` | `number` | Current 1-based page number | | `limit` | `number` | Items per page | Returns `OffsetPaginationMeta` with `totalPages`, `hasNext`, and `hasPrevious`. ### `createOffsetResponse(data, total, page, limit): OffsetPaginatedResponse` Wraps `data` and computed metadata into the standard response envelope. ### `normalizeOffsetRequest(input, defaults?): OffsetPaginationRequest` Clamps `page` to `≥ 1` and `limit` to `[minLimit, maxLimit]`. Defaults: minLimit=1, maxLimit=100, defaultLimit=20, defaultPage=1. ### `pageToOffset(page, limit): number` Returns `Math.max(0, (page - 1) * limit)`. ### `offsetToPage(offset, limit): number` Returns `Math.floor(offset / limit) + 1`. Returns `1` if `limit ≤ 0`. ### `getPageRange(currentPage, totalPages, windowSize?): number[]` Default `windowSize` is 5. The window is centred on `currentPage` and never exceeds `[1, totalPages]`. ### `paginateArray(items, page, limit): OffsetPaginatedResponse` Slices `items[offset..offset+limit]` and builds the full response with computed metadata. ### `estimateTotalCount(adapter, opts): Promise` Samples up to `opts.sampleSize` (default 1000) items from the adapter and extrapolates. Returns `0` if the adapter reports zero total. ## Implementation Notes `normalizeOffsetRequest` uses `Math.floor` to discard fractional page/limit values from untrusted API input. The `sortOrder` field defaults to `"desc"` because most read-heavy UIs show newest items first. `getPageRange` is intentionally view-agnostic — it returns page numbers only; rendering (ellipsis, prev/next buttons) belongs in the UI layer.