Skip to content
This repository was archived by the owner on Sep 10, 2026. It is now read-only.

Follow the server's opaque pagination cursors - #39

Merged
seenu-k merged 4 commits into
mainfrom
feature/opaque-pagination-cursors
Aug 12, 2026
Merged

seenu-k merged 4 commits into
mainfrom
feature/opaque-pagination-cursors

Conversation

@seenu-k

@seenu-k seenu-k commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Client half of eigeninteractive/eigen-server#60. Draft: blocked until that releases.

Blocked on

pubspec.yaml pins eigen_api: ^0.3.0. The published client at that version has int? cursor and no nextCursor, so CI cannot pass. It builds locally only because pubspec_overrides.yaml (gitignored) repoints eigen_api at the sibling eigen-server checkout.

To unblock: merge eigen-server#60 → release → bump the constraint here to the released eigen_api → mark ready.

Changes

The engine now returns an opaque nextCursor on every paged list, null exactly when the list is exhausted. Repositories return a GamesPage record carrying both, and the paging screens hand the token straight back without opening it.

That removes two pieces of server knowledge the client had no business holding:

  • The sort order. history_screen reconstructed the server's ORDER BY as '${last.finishedAt ?? last.updatedAt}' — a hand-maintained copy of a rule the server owns.
  • Where the list ends. lastPage.length < pageSize is a guess, and it's wrong exactly when the final page is full: the reader gets a spinner and a request that returns nothing.

A latent bug this fixes

Both screens now route every refresh through a single _refresh(). The cursor lives beside the paging controller, so the two must be reset together — refreshing without clearing the cursor refetches page one and then continues paging from wherever the last scroll had reached.

This matters more than it looks. lobby_screen merged cleanly against the web-design refactor, which had added five refresh affordances (toolbar, pull-to-refresh, error retry, and a game being joined or cancelled). None of them reset the cursor. A clean merge is exactly how that hides.

Also adds copy for the new invalidCursor code. A cursor is echoed, never composed, so a user can neither cause nor fix one — it means the list needs restarting from the top, which is what a refresh does.

Verification

280 tests pass and the analyzer is clean, against the local override. Neither is meaningful for CI until the constraint is bumped.

🤖 Generated with Claude Code

The engine now returns an opaque `nextCursor` on every paged list, null exactly
when the list is exhausted. Repositories return a `GamesPage` record carrying
both, and the paging screens hand the token straight back without opening it.

This removes two pieces of server knowledge the client had no business holding:

  - the sort order. history_screen reconstructed the server's ORDER BY as
    `'${last.finishedAt ?? last.updatedAt}'`, a copy of a rule the server owns,
    kept in sync by hand.
  - where the list ends. `lastPage.length < pageSize` is a guess, and it is
    wrong exactly when the final page is full: the reader gets a spinner and a
    request that returns nothing.

Both screens now route every refresh through a single `_refresh()`, because the
cursor lives beside the paging controller and the two must be reset together.
Refreshing without clearing the cursor refetches page one and then continues
from wherever the last scroll had reached. history has three refresh
affordances and lobby has five (toolbar, pull-to-refresh, error retry, and on
lobby a game being joined or cancelled), so this was five copies of two lines
waiting to drift.

Adds copy for the new `invalidCursor` code. A cursor is echoed, never composed,
so a user can neither cause nor fix one; it means the list needs restarting
from the top, which is what a refresh does.

Requires eigen_api with `nextCursor` (eigen-server#60).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@seenu-k
seenu-k marked this pull request as ready for review August 12, 2026 15:23
seenu-k and others added 3 commits August 12, 2026 20:57
The engine's 0.4.0 line returns `nextCursor` on every paged response and takes
`cursor` as an opaque string. A shell resolving 0.3.x gets `int? cursor` and no
`nextCursor`, so it cannot read this wire at all.

Also records the pagination work in the changelog, which the previous commit
should have done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The changelog headings are link references and CI checks that each one has a
matching definition, which a hand-written '## [Unreleased]' does not get. cider
maintains those definitions, and the escaping, which is the reason the header
says to use it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
activeGames, playerPublicFinishedGames and friendsGames became async when they
started unwrapping a GamesPage, which changes their generated hashes. The
build_runner + git-diff drift guard is what catches this; I had not re-run it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@seenu-k
seenu-k merged commit a370c49 into main Aug 12, 2026
3 checks passed
@seenu-k
seenu-k deleted the feature/opaque-pagination-cursors branch August 12, 2026 15:41
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant