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​