Fix late background alerts and cache starvation

- Increase background refresh threshold: Raised the edge distance check in GpsController from 1000m to 2500m to guarantee new Overpass data is fetched well before the user drives out of the cached bounding box.

- Fix bounding box race condition: Added await to the background fetch handler in MapView so tracking bounds only shift forward after the network request successfully finishes.

- Make debounced fetches awaitable: Refactored fetchAndUpdate in NodeProviderWithCache to return a Future<void> using a Completer, bridging the 400ms debounce timer.

- Prevent hanging futures: Added state checks for _activeCompleter to instantly resolve canceled network requests during rapid map panning, preventing fatal StateError crashes and infinite UI hangs.
This commit is contained in:
Ryan Brown
2026-08-28 12:03:39 -04:00
parent 3f118a77c4
commit 8439d9bbb6
3 changed files with 23 additions and 9 deletions
+3 -3
View File
@@ -333,9 +333,9 @@ class GpsController {
final minDist = [northDist, southDist, eastDist, westDist].reduce((a, b) => a < b ? a : b);
// Debug 2: Print the closest distance to the edge
debugPrint('[GpsController-Background] Closest edge distance: ${minDist.toStringAsFixed(1)}m (Threshold: 1000m)');
debugPrint('[GpsController-Background] Closest edge distance: ${minDist.toStringAsFixed(1)}m (Threshold: 2500m)');
if (minDist < 1000) {
if (minDist < 2500) {
debugPrint('[GpsController-Background] Threshold crossed. Triggering refresh.');
needsRefresh = true;
}
@@ -554,4 +554,4 @@ class GpsController {
_onMapMovedProgrammatically = null;
_isUserInteracting = null;
}
}
}
+1 -1
View File
@@ -221,7 +221,7 @@ class MapViewState extends State<MapView> with WidgetsBindingObserver {
if (!mounted) return;
final appState = context.read<AppState>();
NodeProviderWithCache.instance.fetchAndUpdate(
await NodeProviderWithCache.instance.fetchAndUpdate(
bounds: bounds,
profiles: appState.enabledProfiles,
uploadMode: appState.uploadMode,
+19 -5
View File
@@ -17,6 +17,7 @@ class NodeProviderWithCache extends ChangeNotifier {
final NodeDataManager _nodeDataManager = NodeDataManager();
Timer? _debounceTimer;
Completer<void>? _activeCompleter;
/// Get cached nodes for the given bounds, filtered by enabled profiles
List<OsmNode> getCachedNodesForBounds(LatLngBounds bounds) {
@@ -34,14 +35,21 @@ class NodeProviderWithCache extends ChangeNotifier {
}
/// Fetch and update nodes for the given view, with debouncing for rapid map movement
void fetchAndUpdate({
Future<void> fetchAndUpdate({
required LatLngBounds bounds,
required List<NodeProfile> profiles,
UploadMode uploadMode = UploadMode.production,
}) {
// Serve cached immediately
notifyListeners();
// Prevent hanging awaits by completing the previous request if it gets cancelled
if (_activeCompleter != null && !_activeCompleter!.isCompleted) {
_activeCompleter!.complete();
}
_activeCompleter = Completer<void>();
// Debounce rapid panning/zooming
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 400), () async {
@@ -52,15 +60,21 @@ class NodeProviderWithCache extends ChangeNotifier {
uploadMode: uploadMode,
isUserInitiated: true,
);
// Notify UI of new data
notifyListeners();
} catch (e) {
debugPrint('[NodeProviderWithCache] Node fetch failed: $e');
// Cache already holds whatever is available for the view
} finally {
// Ensure we only complete if it hasn't been superseded by another call
if (_activeCompleter != null && !_activeCompleter!.isCompleted) {
_activeCompleter!.complete();
}
}
});
return _activeCompleter!.future;
}
/// Clear the cache and repopulate with pending nodes from upload queue
@@ -108,4 +122,4 @@ class NodeProviderWithCache extends ChangeNotifier {
}
return true;
}
}
}