Skip to content

feat: EntityPagination.onEvent — an optional hook of what is being fetched - #147

Merged
gmpassos merged 1 commit into
masterfrom
entity-pagination-events
Aug 2, 2026
Merged

feat: EntityPagination.onEvent — an optional hook of what is being fetched#147
gmpassos merged 1 commit into
masterfrom
entity-pagination-events

Conversation

@gmpassos

@gmpassos gmpassos commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Adds an optional hook to EntityPagination, notified of what is being fetched — for progress reporting and logging over a paginated read.

var p = accountRepository.paginateByQuery(' address.state == ? ',
    parameters: ['NY'], limit: 20, onEvent: (event) {
  switch (event) {
    case EntityPaginationPageLoading(:var page):
      print('fetching page $page...');
    case EntityPaginationPageLoaded(:var page, :var entriesLength):
      print('page $page: $entriesLength entries');
    case EntityPaginationPageError(:var page, :var error):
      print('page $page failed: $error');
    case EntityPaginationPageSkipped(:var page, :var reason):
      print('page $page not fetched: ${reason.name}');
    case EntityPaginationEnd(:var totalLength):
      print('done: $totalLength entries');
    case EntityPaginationReset(:var discardedPages):
      print('discarded ${discardedPages.length} pages');
  }
});

The events

EntityPaginationEvent<O> is sealed, so a switch over it is exhaustive.
Everything but the reset is an EntityPaginationPageEvent, carrying the page.

Event Payload
EntityPaginationPageLoading — emitted once per actual fetch
EntityPaginationPageLoaded entries, entriesLength, elapsedTime, isFinalPage
EntityPaginationPageError error, stackTrace, elapsedTime
EntityPaginationPageSkipped reason: alreadyLoaded / inFlight / knownEmpty
EntityPaginationEnd finalPage, totalLength
EntityPaginationReset discardedPages, discardedEntitiesLength, isRefresh

Design notes

  • Synchronous delivery. EntityPagination supports a synchronous EntityPageLoader (loadPage has a sync fast path), so a Stream would deliver every event in a later microtask — after a sync read already finished. A callback is the right primitive here; a stream is one line on top of it: onEvent: myEventStream.add.
  • onEvent is not final, so it can also be attached to an already built EntityPagination. Only events emitted afterwards are seen.
  • A listener that throws is reported to the current Zone and does not break the fetch.
  • Nothing is allocated while onEvent is null — not even the fetch timer.
  • _setPage now reports whether it resolved the final page, so EntityPaginationEnd is emitted exactly once and after the EntityPaginationPageLoaded that caused it (rather than from inside _setPage, i.e. before it).
  • EntityPaginationReset is emitted after the state is cleared, so the pagination already reads as empty and the discarded state travels on the event. isRefresh distinguishes refresh() (which re-fetches those pages right after) from a pagination that was simply emptied.
  • Concurrent page loads interleave: getRange and refresh start every page at once, so all the fetches are announced before any completes. Asserted in the tests.

paginateByQuery, paginate and paginateAll gained the optional onEvent parameter on EntitySource, EntityRepository and APIRepository, so the hook is reachable without building an EntityPagination by hand.

Tests

15 new cases in bones_api_entity_pagination_test.dart (37 → 52): the event sequence of a full read, of an exact multiple of the page size, of an empty result and of a failure; the 3 skip reasons; the synchronous delivery; a listener attached after construction; a throwing listener; and the reset/refresh events.

Full suite green: 686 passing + ensure_build, no failures. Generated code is unaffected (build_runner reports all 5 outputs same).

Version bumped 1.12.0 → 1.13.0 (pubspec.yaml, BonesAPI.VERSION, CHANGELOG), with the hook documented in the README's pagination section.

🤖 Generated with Claude Code

…tched

- New `EntityPaginationEvent<O>`, a `sealed` hierarchy so a `switch` over it is
  exhaustive, with `EntityPaginationListener<O>` as the callback type:
  `EntityPaginationPageLoading`, `EntityPaginationPageLoaded` (entries,
  elapsedTime, isFinalPage), `EntityPaginationPageError` (error, stackTrace),
  `EntityPaginationPageSkipped` (`alreadyLoaded` / `inFlight` / `knownEmpty`),
  `EntityPaginationEnd` (finalPage, totalLength) and `EntityPaginationReset`
  (discardedPages, discardedEntitiesLength, isRefresh).
  Everything but the reset is an `EntityPaginationPageEvent`, carrying the page.

- Delivered synchronously and in order, so the sequence is also correct for a
  synchronous `EntityPageLoader` (a `Stream` would only deliver in a later
  microtask, after a sync read already finished). Forward it to get a stream:
  `onEvent: myEventStream.add`.

- `onEvent` is not `final`, so it can be attached after construction. A
  listener that throws is reported to the current `Zone` and does not break the
  fetch. Nothing is allocated (not even the fetch timer) while it is `null`.

- `paginateByQuery`, `paginate` and `paginateAll` gained the optional `onEvent`
  parameter, on `EntitySource`, `EntityRepository` and `APIRepository`.

- `_setPage` now reports whether it resolved the final page, so
  `EntityPaginationEnd` is emitted exactly once and *after* the
  `EntityPaginationPageLoaded` that caused it.

- Version 1.12.0 -> 1.13.0. 15 new tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.77778% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.77%. Comparing base (8a81d56) to head (4c108a0).

Files with missing lines Patch % Lines
lib/src/bones_api_entity_pagination.dart 77.77% 16 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #147      +/-   ##
==========================================
+ Coverage   67.68%   67.77%   +0.08%     
==========================================
  Files          64       64              
  Lines       21544    21607      +63     
==========================================
+ Hits        14583    14644      +61     
- Misses       6961     6963       +2     
Flag Coverage Δ
unittests 67.77% <77.77%> (+0.08%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gmpassos
gmpassos merged commit 4caf237 into master Aug 2, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant