@studnicky/paginator
Cursor/page-list state tracker for paginated data sources.
Install
bash
pnpm add @studnicky/paginatorUsage
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.
| Hook | When it fires | Args |
|---|---|---|
onTransition(from, to, event) | After a successful state transition, before the new state is returned | from: 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 exhausted | state: 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.