From 8439d9bbb6ac706f745905a1e6279983b52656f9 Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Fri, 28 Aug 2026 12:03:39 -0400 Subject: [PATCH] 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 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. --- lib/widgets/map/gps_controller.dart | 6 +++--- lib/widgets/map_view.dart | 2 +- lib/widgets/node_provider_with_cache.dart | 24 ++++++++++++++++++----- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/lib/widgets/map/gps_controller.dart b/lib/widgets/map/gps_controller.dart index 9c47844..02402fb 100644 --- a/lib/widgets/map/gps_controller.dart +++ b/lib/widgets/map/gps_controller.dart @@ -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; } -} \ No newline at end of file +} diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index 4d28751..3e20b2a 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -221,7 +221,7 @@ class MapViewState extends State with WidgetsBindingObserver { if (!mounted) return; final appState = context.read(); - NodeProviderWithCache.instance.fetchAndUpdate( + await NodeProviderWithCache.instance.fetchAndUpdate( bounds: bounds, profiles: appState.enabledProfiles, uploadMode: appState.uploadMode, diff --git a/lib/widgets/node_provider_with_cache.dart b/lib/widgets/node_provider_with_cache.dart index 09ff7ce..105bc6c 100644 --- a/lib/widgets/node_provider_with_cache.dart +++ b/lib/widgets/node_provider_with_cache.dart @@ -17,6 +17,7 @@ class NodeProviderWithCache extends ChangeNotifier { final NodeDataManager _nodeDataManager = NodeDataManager(); Timer? _debounceTimer; + Completer? _activeCompleter; /// Get cached nodes for the given bounds, filtered by enabled profiles List 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 fetchAndUpdate({ required LatLngBounds bounds, required List 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(); + // 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; } -} \ No newline at end of file +}