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

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.