From 0d38c116760b23a894b2ae96c5695388c1bdeb4c Mon Sep 17 00:00:00 2001 From: Seenu K Date: Wed, 12 Aug 2026 20:06:14 +0530 Subject: [PATCH 1/4] Follow the server's opaque pagination cursors 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 --- lib/core/api/games_page.dart | 17 ++++++ lib/core/errors/error_messages.dart | 4 ++ lib/features/game/data/game_repository.dart | 37 +++++++----- .../presentation/screens/history_screen.dart | 46 +++++++++------ .../presentation/screens/lobby_screen.dart | 56 ++++++++++++------- .../game/providers/game_providers.dart | 7 ++- .../social/data/social_repository.dart | 7 ++- .../social/providers/social_providers.dart | 4 +- test/shared/api_contract_test.dart | 2 + 9 files changed, 120 insertions(+), 60 deletions(-) create mode 100644 lib/core/api/games_page.dart diff --git a/lib/core/api/games_page.dart b/lib/core/api/games_page.dart new file mode 100644 index 0000000..41781e0 --- /dev/null +++ b/lib/core/api/games_page.dart @@ -0,0 +1,17 @@ +import 'package:eigen_api/eigen_api.dart'; + +/// One page of a paged game list, plus the token that continues it. +/// +/// [nextCursor] is opaque: it comes from the server and goes back to the server +/// unread. That is the point of it. The previous design sent the sort value of +/// the last row, which meant every screen that paged had to know how the server +/// sorted that particular list - that finished games order by their finish time +/// and fall back to their last update, while active ones order by their last +/// update. Two copies of a rule the server owns, kept in sync by hand. +/// +/// [nextCursor] being null is an *answer*: the server has told us this is the +/// end of the list. The screens used to infer that from a page coming back +/// shorter than the page size, which is a guess, and it is wrong exactly when +/// the final page happens to be full - the reader then sees a spinner and one +/// pointless request that returns nothing. +typedef GamesPage = ({List games, String? nextCursor}); diff --git a/lib/core/errors/error_messages.dart b/lib/core/errors/error_messages.dart index 62119d1..16662c2 100644 --- a/lib/core/errors/error_messages.dart +++ b/lib/core/errors/error_messages.dart @@ -55,6 +55,10 @@ String messageForCode(ErrorCode code) => switch (code) { ErrorCode.unsupportedImageType => 'Use a JPEG, PNG, or WebP image.', ErrorCode.rateLimited => "You're doing that too quickly. Wait a moment and try again.", + // A cursor is echoed back, never composed, so a user can neither cause nor + // fix this. It means a paged list needs restarting from the top, which is + // what a refresh does, so the copy asks for exactly that. + ErrorCode.invalidCursor => 'That list is out of date. Pull to refresh.', ErrorCode.unknownDefaultOpenApi => _unexpected, }; diff --git a/lib/features/game/data/game_repository.dart b/lib/features/game/data/game_repository.dart index 06bc97c..984da67 100644 --- a/lib/features/game/data/game_repository.dart +++ b/lib/features/game/data/game_repository.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:eigen_api/eigen_api.dart'; import 'package:eigen_flutter/core/api/engine_call.dart'; +import 'package:eigen_flutter/core/api/games_page.dart'; import 'package:eigen_flutter/core/api/game_socket.dart'; import 'package:eigen_flutter/core/game/game_session.dart'; @@ -41,34 +42,35 @@ class GameRepository { /// Public games waiting for players, newest first. /// - /// [cursor] is the previous page's last `created_at`; omit it for the first - /// page. Paging by cursor rather than offset keeps a page stable while the - /// lobby churns underneath the reader - with an offset, one new game shifts - /// every subsequent page by one and a scroll shows the same row twice. - Future> getLobby({ + /// [cursor] is the previous page's [GamesPage.nextCursor]; omit it for the + /// first page. Paging by cursor rather than offset keeps a page stable while + /// the lobby churns underneath the reader - with an offset, one new game + /// shifts every subsequent page by one and a scroll shows the same row twice. + Future getLobby({ int limit = lobbyPageSize, - int? cursor, + String? cursor, }) async { final body = await engineData( () => _api.getLobby(limit: limit, cursor: cursor), ); - return body.games; + return (games: body.games.toList(), nextCursor: body.nextCursor); } /// The caller's games in one bucket: `active` (still playable) or `finished` /// (the history list). /// - /// [cursor] is the previous page's last sort value - `updated_at` for active - /// games, `finished_at` (falling back to `updated_at`) for finished ones. - Future> getMyGames({ + /// [cursor] is the previous page's [GamesPage.nextCursor]; omit it for the + /// first page. How each bucket is ordered is the server's business and is not + /// restated here. + Future getMyGames({ String bucket = activeGamesBucket, int limit = historyPageSize, - int? cursor, + String? cursor, }) async { final body = await engineData( () => _api.getMyGames(bucket: bucket, limit: limit, cursor: cursor), ); - return body.games; + return (games: body.games.toList(), nextCursor: body.nextCursor); } /// One game's index entry: what discovery and history show. @@ -93,14 +95,19 @@ class GameRepository { /// /// Any player, human or bot. Public and finished only, so this exposes /// nothing that was not already replayable by anyone holding the game's id. - Future> getPlayerGames( + Future getPlayerGames( String playerId, { int? limit, + String? cursor, }) async { final body = await engineData( - () => _players.getPlayerGames(playerId: playerId, limit: limit), + () => _players.getPlayerGames( + playerId: playerId, + limit: limit, + cursor: cursor, + ), ); - return body.games; + return (games: body.games.toList(), nextCursor: body.nextCursor); } /// The bots available to seat, for this build's schema version. diff --git a/lib/features/game/presentation/screens/history_screen.dart b/lib/features/game/presentation/screens/history_screen.dart index 6b08766..fe301d4 100644 --- a/lib/features/game/presentation/screens/history_screen.dart +++ b/lib/features/game/presentation/screens/history_screen.dart @@ -55,28 +55,42 @@ class HistoryScreen extends ConsumerStatefulWidget { class _HistoryScreenState extends ConsumerState { late final PagingController _pagingController; + /// The cursor for the page after the one most recently fetched, or null once + /// the server has said the list is exhausted. The empty string is "no cursor + /// yet", i.e. the first page; it cannot be null, because null is how this + /// controller is told there are no more pages. + String? _nextKey = ''; + + /// Reload from the top. + /// + /// The cursor lives beside the controller rather than inside it, so the two + /// have to be reset together - refreshing the list without clearing the + /// cursor would refetch page one and then continue from wherever the last + /// scroll had reached. There are three refresh affordances on this screen + /// (the toolbar button, pull-to-refresh, and the error retry), which is + /// exactly why this is a method and not three copies of two lines. + void _refresh() { + _nextKey = ''; + _pagingController.refresh(); + } + @override void initState() { super.initState(); _pagingController = PagingController( - getNextPageKey: (state) { - final pages = state.pages; - if (pages == null || pages.isEmpty) return ''; - final lastPage = pages.last; - if (lastPage.length < historyPageSize) return null; - // The server pages by keyset: the next page is everything strictly - // older than this page's last sort value, which for finished games is - // the finish time (falling back to the last update for aborted ones). - final last = lastPage.last.game; - return '${last.finishedAt ?? last.updatedAt}'; - }, + getNextPageKey: (state) => _nextKey, fetchPage: (key) async { - final games = await ref + final page = await ref .read(gameRepositoryProvider) .getMyGames( bucket: finishedGamesBucket, - cursor: key.isEmpty ? null : int.parse(key), + cursor: key.isEmpty ? null : key, ); + // The server says where the next page starts, and says so with a token + // this screen never opens. Nothing here knows that finished games sort + // by their finish time; that is the server's rule to keep. + _nextKey = page.nextCursor; + final games = page.games; // The summary carries the roster, the outcomes and the rating deltas, // so every field of a row is derived from the one response. Joining a // separate rating log here would have been a second round trip per @@ -134,7 +148,7 @@ class _HistoryScreenState extends ConsumerState { child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: IconButton( - onPressed: _pagingController.refresh, + onPressed: _refresh, icon: const Icon(Icons.refresh), tooltip: 'Refresh history', ), @@ -142,7 +156,7 @@ class _HistoryScreenState extends ConsumerState { ), Expanded( child: RefreshIndicator( - onRefresh: () async => _pagingController.refresh(), + onRefresh: () async => _refresh(), child: PagingListener( controller: _pagingController, builder: (context, state, fetchNextPage) { @@ -174,7 +188,7 @@ class _HistoryScreenState extends ConsumerState { Text(humanize(state.error ?? 'Unknown error')), const SizedBox(height: 16), FilledButton( - onPressed: _pagingController.refresh, + onPressed: _refresh, child: const Text('Retry'), ), ], diff --git a/lib/features/game/presentation/screens/lobby_screen.dart b/lib/features/game/presentation/screens/lobby_screen.dart index 5313c2b..dbaae4e 100644 --- a/lib/features/game/presentation/screens/lobby_screen.dart +++ b/lib/features/game/presentation/screens/lobby_screen.dart @@ -5,7 +5,6 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:eigen_flutter/core/errors/error_messages.dart'; import 'package:eigen_flutter/core/adaptive/adaptive_layout.dart'; import 'package:eigen_flutter/features/auth/providers/auth_providers.dart'; -import 'package:eigen_flutter/features/game/data/game_repository.dart'; import 'package:eigen_flutter/features/game/providers/game_providers.dart'; import 'package:eigen_flutter/features/game/utils/game_timing.dart'; @@ -189,6 +188,26 @@ class _LobbyTabContentState extends ConsumerState<_LobbyTabContent> with AutomaticKeepAliveClientMixin { late final PagingController _pagingController; + /// The cursor for the page after the one most recently fetched, or null once + /// the server has said the list is exhausted. The empty string is "no cursor + /// yet", i.e. the first page; it cannot be null, because null is how this API + /// says there are no more pages. + String? _nextKey = ''; + + /// Reload from the top. + /// + /// The cursor lives beside the controller rather than inside it, so the two + /// have to be reset together - refreshing without clearing the cursor would + /// refetch page one and then continue paging from wherever the last scroll + /// had reached. This screen has five refresh affordances (the toolbar button, + /// pull-to-refresh, the error retry, and a game being joined or cancelled), + /// which is exactly why this is a method rather than five copies of two + /// lines. + void _refresh() { + _nextKey = ''; + _pagingController.refresh(); + } + @override bool get wantKeepAlive => true; @@ -196,21 +215,16 @@ class _LobbyTabContentState extends ConsumerState<_LobbyTabContent> void initState() { super.initState(); _pagingController = PagingController( - getNextPageKey: (state) { - final pages = state.pages; - if (pages == null || pages.isEmpty) return ''; - final lastPage = pages.last; - if (lastPage.length < lobbyPageSize) return null; - return '${lastPage.last.createdAt}'; - }, - fetchPage: (key) { - final cursor = key.isEmpty ? null : int.parse(key); - if (widget.mode == _LobbyMode.public) { - return ref.read(gameRepositoryProvider).getLobby(cursor: cursor); - } - return ref - .read(socialRepositoryProvider) - .getFriendsGames(cursor: cursor); + getNextPageKey: (state) => _nextKey, + fetchPage: (key) async { + final cursor = key.isEmpty ? null : key; + final page = widget.mode == _LobbyMode.public + ? await ref.read(gameRepositoryProvider).getLobby(cursor: cursor) + : await ref + .read(socialRepositoryProvider) + .getFriendsGames(cursor: cursor); + _nextKey = page.nextCursor; + return page.games; }, ); _pagingController.addListener(_onPagingError); @@ -256,7 +270,7 @@ class _LobbyTabContentState extends ConsumerState<_LobbyTabContent> child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: IconButton( - onPressed: _pagingController.refresh, + onPressed: _refresh, icon: const Icon(Icons.refresh), tooltip: 'Refresh games', ), @@ -264,7 +278,7 @@ class _LobbyTabContentState extends ConsumerState<_LobbyTabContent> ), Expanded( child: RefreshIndicator( - onRefresh: () async => _pagingController.refresh(), + onRefresh: () async => _refresh(), child: PagingListener( controller: _pagingController, builder: (context, state, fetchNextPage) { @@ -274,8 +288,8 @@ class _LobbyTabContentState extends ConsumerState<_LobbyTabContent> itemBuilder: (context, item, index) => _GameCard( key: ValueKey(item.id), game: item, - onCancelled: _pagingController.refresh, - onJoined: _pagingController.refresh, + onCancelled: _refresh, + onJoined: _refresh, ), noItemsFoundIndicatorBuilder: (_) => EmptyStateView( icon: Icons.sports_esports_outlined, @@ -305,7 +319,7 @@ class _LobbyTabContentState extends ConsumerState<_LobbyTabContent> Text(humanize(state.error ?? 'Unknown error')), const SizedBox(height: 16), FilledButton( - onPressed: _pagingController.refresh, + onPressed: _refresh, child: const Text('Retry'), ), ], diff --git a/lib/features/game/providers/game_providers.dart b/lib/features/game/providers/game_providers.dart index a4c6cdf..40ccd69 100644 --- a/lib/features/game/providers/game_providers.dart +++ b/lib/features/game/providers/game_providers.dart @@ -129,7 +129,7 @@ bool soloPlayAvailable(Ref ref) { /// the deadline, so nothing has to be derived from a second read. @riverpod Future> activeGames(Ref ref) async { - final games = await ref.watch(gameRepositoryProvider).getMyGames(); + final games = (await ref.watch(gameRepositoryProvider).getMyGames()).games; final myUserId = ref.watch(currentUserIdProvider); bool isMyTurn(GameSummary game) { @@ -347,6 +347,7 @@ List gameOutcomes(Ref ref, {required String gameId}) { Future> playerPublicFinishedGames( Ref ref, { required String playerId, -}) { - return ref.watch(gameRepositoryProvider).getPlayerGames(playerId); +}) async { + final page = await ref.watch(gameRepositoryProvider).getPlayerGames(playerId); + return page.games; } diff --git a/lib/features/social/data/social_repository.dart b/lib/features/social/data/social_repository.dart index 8fe73a5..11902a9 100644 --- a/lib/features/social/data/social_repository.dart +++ b/lib/features/social/data/social_repository.dart @@ -1,5 +1,6 @@ import 'package:eigen_api/eigen_api.dart'; import 'package:eigen_flutter/core/api/engine_call.dart'; +import 'package:eigen_flutter/core/api/games_page.dart'; /// The friend graph: friends, pending requests, blocks, and user search. /// @@ -29,13 +30,13 @@ class SocialRepository { /// Joinable games created by the caller's friends. /// - /// [cursor] is the previous page's last `created_at`; omit for the first + /// [cursor] is the previous page's [GamesPage.nextCursor]; omit for the first /// page. - Future> getFriendsGames({int? limit, int? cursor}) async { + Future getFriendsGames({int? limit, String? cursor}) async { final body = await engineData( () => _api.getFriendsGames(limit: limit, cursor: cursor), ); - return body.games; + return (games: body.games.toList(), nextCursor: body.nextCursor); } /// Case-insensitive search over usernames and display names. diff --git a/lib/features/social/providers/social_providers.dart b/lib/features/social/providers/social_providers.dart index d547c6c..b97cfc7 100644 --- a/lib/features/social/providers/social_providers.dart +++ b/lib/features/social/providers/social_providers.dart @@ -138,8 +138,8 @@ Future> outgoingRequests(Ref ref) async { /// Joinable games created by the caller's friends. @riverpod -Future> friendsGames(Ref ref) { - return ref.watch(socialRepositoryProvider).getFriendsGames(); +Future> friendsGames(Ref ref) async { + return (await ref.watch(socialRepositoryProvider).getFriendsGames()).games; } /// The current relationship between the local user and another player. diff --git a/test/shared/api_contract_test.dart b/test/shared/api_contract_test.dart index d333428..13e267f 100644 --- a/test/shared/api_contract_test.dart +++ b/test/shared/api_contract_test.dart @@ -45,6 +45,8 @@ void main() { 'unsupportedImageType', // Per-user write rate limiting (429). 'rateLimited', + // A pagination cursor that did not decode (400). + 'invalidCursor', }); }); From ce35dabd697ea9f7090d21da1c5e63f14d4c96a0 Mon Sep 17 00:00:00 2001 From: Seenu K Date: Wed, 12 Aug 2026 20:57:21 +0530 Subject: [PATCH 2/4] Require eigen_api ^0.4.0 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 --- CHANGELOG.md | 11 +++++++++++ pubspec.yaml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8b6124..1d1a144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,17 @@ Pre-1.0, breaking changes land in a **MINOR** bump: `^0.1.0` resolves to [Versions and compatibility](https://eigeninteractive.com/docs/reference/compatibility) for how this package, the engine and the generated `eigen_api` client pair up. +## [Unreleased] +### Changed +- Paged lists follow the engine's opaque cursors. `getLobby`, `getMyGames`, `getPlayerGames` and `getFriendsGames` return a `GamesPage` record carrying the page and the server's `nextCursor`, instead of a bare list. That removes two things the client had no business knowing: the sort order, which history\_screen reconstructed as the last row's `finishedAt ?? updatedAt`, a hand-maintained copy of a rule the server owns; and where the list ends, which was inferred from a page coming back shorter than the page size. The inference is wrong exactly when the final page is full, and costs the reader a spinner and a request that returns nothing. `nextCursor` is null when the list is exhausted, so it is an answer rather than a guess. +- eigen\_api pin raised to ^0.4.0: the engine now returns `nextCursor` on every paged response and takes `cursor` as an opaque string, and a shell speaking 0.3.x cannot read it. + +### Fixed +- Refreshing a paged list no longer pages on from a stale cursor. The cursor lives beside the paging controller, so the two have to be reset together; refreshing without clearing it refetched page one and then continued from wherever the last scroll had reached. Both screens now route every refresh through one \_refresh(), which matters because history has three refresh affordances and the lobby has five (toolbar, pull-to-refresh, error retry, and on the lobby a game being joined or cancelled). + +### Added +- Copy for the engine's 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. + ## [0.5.0] - 2026-08-12 ## [0.4.1] - 2026-08-12 ### Added diff --git a/pubspec.yaml b/pubspec.yaml index f46235f..7eaf31e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: # Pre-1.0 that resolves to `>=0.1.0 <0.2.0`: the engine announces breakage in # the MINOR position, so 0.2.0 is the release that needs a deliberate bump # here. See https://eigeninteractive.com/docs/reference/compatibility. - eigen_api: ^0.3.0 + eigen_api: ^0.4.0 dio: ^5.10.0 dio_smart_retry: ^7.0.1 firebase_auth: ^6.5.6 From bae7287ec5e5c8a929849c34c7b50f367c71ff74 Mon Sep 17 00:00:00 2001 From: Seenu K Date: Wed, 12 Aug 2026 21:02:45 +0530 Subject: [PATCH 3/4] Record the pagination work with cider rather than by hand 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 --- CHANGELOG.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d1a144..b33115a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,16 +15,16 @@ Pre-1.0, breaking changes land in a **MINOR** bump: `^0.1.0` resolves to for how this package, the engine and the generated `eigen_api` client pair up. ## [Unreleased] +### Added +- Copy for the engine's 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. + ### Changed -- Paged lists follow the engine's opaque cursors. `getLobby`, `getMyGames`, `getPlayerGames` and `getFriendsGames` return a `GamesPage` record carrying the page and the server's `nextCursor`, instead of a bare list. That removes two things the client had no business knowing: the sort order, which history\_screen reconstructed as the last row's `finishedAt ?? updatedAt`, a hand-maintained copy of a rule the server owns; and where the list ends, which was inferred from a page coming back shorter than the page size. The inference is wrong exactly when the final page is full, and costs the reader a spinner and a request that returns nothing. `nextCursor` is null when the list is exhausted, so it is an answer rather than a guess. -- eigen\_api pin raised to ^0.4.0: the engine now returns `nextCursor` on every paged response and takes `cursor` as an opaque string, and a shell speaking 0.3.x cannot read it. +- Paged lists follow the engine's opaque cursors. getLobby, getMyGames, getPlayerGames and getFriendsGames return a GamesPage record carrying the page and the server's nextCursor, instead of a bare list. That removes two things the client had no business knowing: the sort order, which history\_screen reconstructed from the last row's finishedAt ?? updatedAt, a hand-maintained copy of a rule the server owns; and where the list ends, which was inferred from a page coming back shorter than the page size. That inference is wrong exactly when the final page is full, and costs the reader a spinner and a request that returns nothing. nextCursor is null when the list is exhausted, so it is an answer rather than a guess. +- eigen\_api pin raised to ^0.4.0: the engine now returns nextCursor on every paged response and takes cursor as an opaque string, and a shell speaking 0.3.x cannot read it. ### Fixed - Refreshing a paged list no longer pages on from a stale cursor. The cursor lives beside the paging controller, so the two have to be reset together; refreshing without clearing it refetched page one and then continued from wherever the last scroll had reached. Both screens now route every refresh through one \_refresh(), which matters because history has three refresh affordances and the lobby has five (toolbar, pull-to-refresh, error retry, and on the lobby a game being joined or cancelled). -### Added -- Copy for the engine's 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. - ## [0.5.0] - 2026-08-12 ## [0.4.1] - 2026-08-12 ### Added @@ -178,6 +178,7 @@ server-side concern now live in the engine. - `google_fonts`, which fetched Inter at runtime, replaced by the bundled package font above. +[Unreleased]: https://github.com/eigeninteractive/eigen-flutter/compare/v0.5.0...HEAD [0.5.0]: https://github.com/eigeninteractive/eigen-flutter/compare/v0.4.1...v0.5.0 [0.4.1]: https://github.com/eigeninteractive/eigen-flutter/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/eigeninteractive/eigen-flutter/compare/v0.3.7...v0.4.0 From 24141c6e92a5e3ae7c975f72aaeae7ecdc2b4a0a Mon Sep 17 00:00:00 2001 From: Seenu K Date: Wed, 12 Aug 2026 21:07:20 +0530 Subject: [PATCH 4/4] Regenerate the Riverpod providers 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 --- lib/features/game/providers/game_providers.g.dart | 4 ++-- lib/features/social/providers/social_providers.g.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/features/game/providers/game_providers.g.dart b/lib/features/game/providers/game_providers.g.dart index fc574f8..e7c6428 100644 --- a/lib/features/game/providers/game_providers.g.dart +++ b/lib/features/game/providers/game_providers.g.dart @@ -428,7 +428,7 @@ final class ActiveGamesProvider } } -String _$activeGamesHash() => r'3d2e1b090ecec8292f8d2329cbeb8899ac6b843d'; +String _$activeGamesHash() => r'0937340c6c0063bdeb40a6a06ce7abc114af0e2d'; /// One game's live session: the single subscription a game screen needs. /// @@ -1263,7 +1263,7 @@ final class PlayerPublicFinishedGamesProvider } String _$playerPublicFinishedGamesHash() => - r'75eaf75a9b4b3f2d4b7cb424f2f09cb59f1ac449'; + r'4eb0d86534a5f56d0ea7eeaef5d47e668c07d1e5'; /// A player's most recent finished public games, for the replay list on their /// profile. diff --git a/lib/features/social/providers/social_providers.g.dart b/lib/features/social/providers/social_providers.g.dart index d145c11..4ce1985 100644 --- a/lib/features/social/providers/social_providers.g.dart +++ b/lib/features/social/providers/social_providers.g.dart @@ -314,7 +314,7 @@ final class FriendsGamesProvider } } -String _$friendsGamesHash() => r'd2418a76d6c91fefa247c31f379503f3edb0fd38'; +String _$friendsGamesHash() => r'b66fa0dc6cf0d0304b7b031e4eaca94a35ce7cd3'; @ProviderFor(friendStatus) final friendStatusProvider = FriendStatusFamily._();