Files
skills/docs/foundation/data/pagination/src/offset/offset.md
2026-05-29 08:20:56 +01:00

4.3 KiB
Raw Blame History

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<T> 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<T>.
  • estimateTotalCount — sample-based approximate total count via adapter.

Usage: Basic Example

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

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<T>(data, total, page, limit): OffsetPaginatedResponse<T>

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<T>(items, page, limit): OffsetPaginatedResponse<T>

Slices items[offset..offset+limit] and builds the full response with computed metadata.

estimateTotalCount(adapter, opts): Promise<number>

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.