mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Cancel in-flight paginated fetches to prevent stale-write races
Rapidly switching filter chips, sort columns, or pagination on the devices and notifications lists could overlap fetches, and an older response landing after a newer one would silently overwrite the list with results that no longer match the active query. Thread an optional CancelToken into listDevices and listNotifications. Each State now keeps one active token, cancels it on every new fetch and on dispose, and bails from both success and catch branches when its token has been superseded. DioExceptionType.cancel is silenced so internal cancellations don't surface as user-visible errors. The notifications list's prior `if (_isLoading) return;` guard is removed so the 1-min periodic refresh is no longer skipped during user activity. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
d3b6c64e8b
commit
30df22df56
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../model/device.dart';
|
||||
@@ -35,6 +36,7 @@ class _DeviceListState extends State<DeviceList> with RouteAware {
|
||||
|
||||
int _currentPage = 0;
|
||||
bool _hasNextPage = false;
|
||||
CancelToken? _fetchToken;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -61,6 +63,7 @@ class _DeviceListState extends State<DeviceList> with RouteAware {
|
||||
void dispose() {
|
||||
routeObserver.unsubscribe(this);
|
||||
_ownerDebounce?.cancel();
|
||||
_fetchToken?.cancel();
|
||||
_ownerController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -74,6 +77,9 @@ class _DeviceListState extends State<DeviceList> with RouteAware {
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(int page) async {
|
||||
_fetchToken?.cancel();
|
||||
final token = CancelToken();
|
||||
_fetchToken = token;
|
||||
setState(() {
|
||||
_isLoading = _devices.isEmpty;
|
||||
_error = null;
|
||||
@@ -91,8 +97,9 @@ class _DeviceListState extends State<DeviceList> with RouteAware {
|
||||
sortAscending: _sortAscending,
|
||||
offset: page * _pageSize,
|
||||
limit: _pageSize + 1,
|
||||
cancelToken: token,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (!mounted || token != _fetchToken) return;
|
||||
setState(() {
|
||||
_currentPage = page;
|
||||
_hasNextPage = results.length > _pageSize;
|
||||
@@ -100,7 +107,8 @@ class _DeviceListState extends State<DeviceList> with RouteAware {
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
if (!mounted || token != _fetchToken) return;
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
setState(() {
|
||||
_error = dioErrorToUserMessage(e);
|
||||
_isLoading = false;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../model/notification.dart' as oott_model;
|
||||
@@ -45,6 +46,7 @@ class _NotificationsListState extends State<NotificationsList> with RouteAware {
|
||||
bool _isLoading = false;
|
||||
bool _hasNextPage = false;
|
||||
String? _error;
|
||||
CancelToken? _fetchToken;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -74,11 +76,14 @@ class _NotificationsListState extends State<NotificationsList> with RouteAware {
|
||||
void dispose() {
|
||||
routeObserver.unsubscribe(this);
|
||||
_notificationTimer?.cancel();
|
||||
_fetchToken?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(int page) async {
|
||||
if (_isLoading) return;
|
||||
_fetchToken?.cancel();
|
||||
final token = CancelToken();
|
||||
_fetchToken = token;
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
@@ -88,8 +93,9 @@ class _NotificationsListState extends State<NotificationsList> with RouteAware {
|
||||
_filter.isNew,
|
||||
page * _pageSize,
|
||||
limit: _pageSize + 1,
|
||||
cancelToken: token,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (!mounted || token != _fetchToken) return;
|
||||
setState(() {
|
||||
_currentPage = page;
|
||||
_hasNextPage = results.length > _pageSize;
|
||||
@@ -97,7 +103,8 @@ class _NotificationsListState extends State<NotificationsList> with RouteAware {
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
if (!mounted || token != _fetchToken) return;
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
setState(() {
|
||||
_error = dioErrorToUserMessage(e);
|
||||
_isLoading = false;
|
||||
|
||||
@@ -142,6 +142,7 @@ class BackendAPI {
|
||||
bool? sortAscending,
|
||||
int? offset,
|
||||
int? limit,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
debugPrint('About to call /devices');
|
||||
|
||||
@@ -162,7 +163,11 @@ class BackendAPI {
|
||||
params['page_limit'] = limit ?? _pageSize;
|
||||
}
|
||||
|
||||
final response = await _dio.get('/devices', queryParameters: params);
|
||||
final response = await _dio.get(
|
||||
'/devices',
|
||||
queryParameters: params,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
|
||||
debugPrint('Received: ${response.data}');
|
||||
|
||||
@@ -257,6 +262,7 @@ class BackendAPI {
|
||||
bool? isNew,
|
||||
int offset, {
|
||||
int? limit,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
debugPrint('About to call /notifications');
|
||||
|
||||
@@ -268,6 +274,7 @@ class BackendAPI {
|
||||
'page_offset': offset,
|
||||
'page_limit': limit ?? _pageSize,
|
||||
},
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
|
||||
debugPrint('Received: ${response.data}');
|
||||
|
||||
Reference in New Issue
Block a user