Skip to main content

Pagination

@cyblow/paginate covers both pagination models: offset (page/limit) for in-memory arrays and bounded sets, and keyset (cursor) for large or frequently-changing database queries. It also exposes the portable cursor codec and a resident Dataset.

Offset pagination

Parameters

OffsetParams holds a 1-based page (default 1) and a limit (default 20, range 1..=MAX_LIMIT). Both are validated at construction by the core — an out-of-range value throws ValidationError. The zero-based row offset is derived for you:

import { OffsetParams, MAX_LIMIT } from "@cyblow/paginate";

const params = new OffsetParams({ page: 2, limit: 20 });
params.offset; // 20
MAX_LIMIT; // the shared maximum page size (1000) — a DoS guard

Paginating an array

import { paginate, OffsetParams } from "@cyblow/paginate";

const page = paginate(users, new OffsetParams({ page: 1, limit: 20 }));

page.total; // total rows across all pages (number)
page.page; // the requested page number
page.pages; // total number of pages — ceil(total / limit)
page.limit; // rows per page
page.hasNext; // boolean
page.hasPrevious; // boolean

for (const user of page.items) { /* ... */ }
page.items[0];
page.items.length;

:::note Page shape vs. Python The fields match Python apart from naming style: TypeScript uses hasNext / hasPrevious and you iterate page.items; Python uses has_next / has_previous and the page is directly iterable. See parity. :::

Pages beyond the range, and clamp

Requesting a page past the last one is not an error — you get empty items with the metadata still describing the full dataset. To snap an out-of-range request to the last valid page, OffsetParams offers .clamp(total):

const safe = new OffsetParams({ page: 100, limit: 20 }).clamp(50); // page -> 3

The standalone helpers offset, maxPages, clampPage, and offsetMeta(page, limit, total) are exported if you need the pieces directly (e.g. to build metadata around an ORM count).

Keyset (cursor) pagination

Keyset pagination pages by the ordering values of the last row seen, so it stays correct and fast under writes — see pagination models. It is database-backed; for an in-memory array, offset is simpler and just as fast.

The cursor codec

The codec is exposed directly, and a cursor minted here decodes byte-for-byte in a Python service and back (see cursors):

import { encodeCursor, decodeCursor } from "@cyblow/paginate";

const cursor = encodeCursor([42, "2025-06-01T00:00:00"]);
decodeCursor(cursor); // → [42, "2025-06-01T00:00:00"]

Parameters and result page

CursorParams holds a limit plus at most one of after / before:

import { CursorParams } from "@cyblow/paginate";

new CursorParams({ limit: 20 }); // first page
new CursorParams({ limit: 20, after: cursor }); // page forward
new CursorParams({ limit: 20, before: cursor }); // page backward

A keyset query returns a CursorPage with items, limit, hasNext / hasPrevious, and nextCursor / previousCursor (no total). Feed nextCursor back in as the next request's after.

Driving it

Keyset is driven by the ORM adapters, which render the core's portable keyset predicate:

The resident Dataset

The one-shot helpers marshal your whole array into the core on every call. When you query the same rows repeatedly, a Dataset marshals once, then answers many queries natively. In JS the standout win is the fused page() — filter → sort → paginate in one native columnar pass that returns only the page (see performance).

import { Dataset, OffsetParams } from "@cyblow/paginate";

const ds = new Dataset(users); // marshals once
ds.size; // number of rows held

const adults = ds.filter([{ field: "age", operator: "gte", value: 18 }]);
const newest = ds.sort([{ field: "createdAt", direction: "desc" }]);
const hits = ds.search({ query: "alice", fields: ["name", "email"] });

const page = ds.page(new OffsetParams({ page: 1, limit: 20 }), {
filters: [{ field: "age", operator: "gte", value: 18 }],
sorting: [{ field: "createdAt", direction: "desc" }],
search: { query: "alice", fields: ["name", "email"] },
});
page.total; // matches across all pages
page.items; // the page's rows

:::tip JS performance For a single one-shot filter/sort/search over data your host already holds, native Array methods are often faster than crossing into Rust — use the helpers for behaviour parity, and reach for Dataset.page (the fused pipeline) for the hot path. The benchmarks are explicit about this. :::

Next