4.1 KiB
Page Tokens
(no submodules)
Purpose
The page tokens module provides opaque, URL-safe token strings that carry all the state needed to resume a paginated query: the cursor or offset value, sort field and direction, and an optional filter hash for cache invalidation. Tokens are encoded as base64url JSON so they survive URL query-string round-trips without additional percent-encoding.
This module sits above the raw cursor encoding in cursor-pagination.ts: where that module encodes a single value, page tokens encode a structured payload that includes sort and filter metadata — making them suitable for stateless pagination APIs where the client must carry all context.
Features
encodePageToken/decodePageToken— full payload round-trip with type validation.createCursorToken— create a token from a domain value (ID, score, timestamp) and optional sort/filter info.createOffsetToken— shortcut for offset-based tokens.extractCursorValue— pull just the value field from a token.extractSortInfo— pull sort field and direction from a token.hashFilters— stable djb2 hash of a filter map for cache-invalidation.validateTokenFilter— check that a token's filter hash matches an expected value.
Usage: Basic Example
import { createCursorToken, decodePageToken } from "@suaveplan/pagination/tokens";
const token = createCursorToken("user_99", "createdAt", "desc");
// → opaque base64url string
const payload = decodePageToken(token);
// → { v: "user_99", s: "createdAt", d: "desc" }
Usage: Advanced Example
import {
createCursorToken,
hashFilters,
validateTokenFilter,
extractSortInfo,
} from "@suaveplan/pagination/tokens";
const filters = { status: "active", role: "admin" };
const hash = hashFilters(filters);
// → stable hash string, e.g. "1q7f9k"
const token = createCursorToken("cursor_abc", "name", "asc", hash);
// On the next request, verify the filter hasn't changed
const valid = validateTokenFilter(token, hash); // → true
const invalid = validateTokenFilter(token, "differenthash"); // → false
// Extract sort metadata to rebuild the query
const sort = extractSortInfo(token);
// → { sortBy: "name", sortDirection: "asc" }
API Reference
encodePageToken(payload: PageTokenPayload): string
Serialises the payload to JSON and encodes as base64url. The payload must have a v field (string or number); s, d, f are optional.
decodePageToken(token: string): PageTokenPayload | null
Decodes and validates a token. Returns null for any invalid input, never throws.
createCursorToken(value, sortBy?, sortDirection?, filterHash?): string
Builds and encodes a PageTokenPayload from a cursor value and optional sort/filter metadata.
createOffsetToken(offset, sortBy?, sortDirection?, filterHash?): string
Alias of createCursorToken with offset: number as the first argument.
extractCursorValue(token: string): string | number | null
Returns the v field of the decoded token, or null if the token is invalid.
extractSortInfo(token: string): { sortBy?: string; sortDirection?: SortDirection } | null
Returns the s and d fields of the decoded token, or null if the token is invalid.
hashFilters(filters: Record<string, unknown>): string
Sorts keys lexicographically, concatenates key=value pairs, and applies a djb2 hash. Returns the absolute hash as a base-36 string. Suitable for cache-invalidation only — not cryptographic.
validateTokenFilter(token: string, expectedFilterHash: string): boolean
Returns true when the token is valid and its f field equals expectedFilterHash. Returns true (unconditional) when the token has no f field. Returns false for invalid tokens or mismatched hashes.
Implementation Notes
The djb2 hash in hashFilters uses bitwise OR with 0 to keep the accumulator within signed 32-bit integer range — preventing floating-point precision loss for long filter strings. The filter hash is NOT a cryptographic MAC; it cannot prevent token forgery. Use HMAC-signed tokens if forgery resistance is required.