Skip to content

@studnicky/paginator

Cursor/page-list state tracker for paginated data sources.

Install

bash
pnpm add @studnicky/paginator

Usage

Paginator does not fetch data — the caller supplies each fetched page via next(page, nextCursor), and the tracker reports whether more pages are expected and holds the pages received so far:

ts
import type { PaginatorEventType, PaginatorStateType } from '../src/index.js';
import type { TransitionRecordEntity } from './entities/TransitionRecordEntity.js';

import { Paginator } from '../src/index.js';

class TelemetryPaginator<TPage, TCursor> extends Paginator<TPage, TCursor> {
  readonly transitions: TransitionRecordEntity.Type[] = [];

  static tracked<TPage, TCursor>(): TelemetryPaginator<TPage, TCursor> {
    return new TelemetryPaginator<TPage, TCursor>();
  }

  protected override onTransition(
    from: PaginatorStateType<TPage, TCursor>,
    to: PaginatorStateType<TPage, TCursor>,
    event: PaginatorEventType<TPage, TCursor>
  ): void {
    console.log(`[paginator] ${from.variant} --${event.type}--> ${to.variant}`);
    this.transitions.push({ 'event': event.type, 'from': from.variant, 'to': to.variant });
  }

  protected override onEnterState(state: PaginatorStateType<TPage, TCursor>): void {
    console.log(`[paginator] entered ${state.variant}`);
  }

  protected override onExitState(state: PaginatorStateType<TPage, TCursor>): void {
    console.log(`[paginator] exited ${state.variant}`);
  }
}

const paginator = TelemetryPaginator.tracked<string, number>();

// idle -> hasMore
paginator.next('page-1', 2);
// hasMore -> hasMore (no transition hooks fire — same variant)
paginator.next('page-2', 3);
// hasMore -> exhausted
paginator.next('page-3', undefined);
// exhausted -> idle
paginator.reset();

console.log('Pages:', paginator.pages);
console.log('Transitions:', paginator.transitions);

Observability hooks

Subclass Paginator and override any protected hook to inject trace logging, metrics, or side-effects at the exact stage where they are needed. Hooks should stay fast and non-blocking; observer-hook failures are contained so pagination state still wins.

HookWhen it firesArgs
onTransition(from, to, event)After a successful state transition, before the new state is returnedfrom: PaginatorStateType<TPage, TCursor>, to: PaginatorStateType<TPage, TCursor>, event: PaginatorEventType<TPage, TCursor>
onEnterState(state)When entering a new state variant (not called when the variant is unchanged)state: PaginatorStateType<TPage, TCursor>
onExitState(state)When exiting a state variant (not called when the variant is unchanged)state: PaginatorStateType<TPage, TCursor>
onTransitionRejected(state, event, reason)When next() is called after the source is already exhaustedstate: PaginatorStateType<TPage, TCursor>, event: PaginatorEventType<TPage, TCursor>, reason: string

The base class never calls any logger or metrics library. All hooks are no-ops by default.

Source on GitHub