[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: request deduplication and cancellation

+92 -24
+92 -24
lib/src/features/feed/providers/feed_provider.dart
··· 29 29 late final SparkLogger _logger; 30 30 late final DownloadManagerInterface _downloadManager; 31 31 32 + // Track active fetch operation for cancellation 33 + int _fetchGeneration = 0; 34 + 32 35 // Add a flag to track if this notifier has been built before 33 36 bool _hasBeenBuilt = false; 34 37 FeedState? _preservedState; 35 38 36 39 @override 37 40 FeedState build(Feed feed) { 41 + // Track previous feed to detect changes 42 + final previousFeed = _hasBeenBuilt ? _feed : null; 38 43 _feed = feed; 44 + 45 + // If feed changed, increment generation to cancel any pending fetches 46 + if (previousFeed != null && previousFeed.config.id != feed.config.id) { 47 + _fetchGeneration++; 48 + _logger.d( 49 + 'Feed changed from ${previousFeed.config.id} to ${feed.config.id}, ' 50 + 'incremented generation to $_fetchGeneration', 51 + ); 52 + } 39 53 40 54 // Initialize logger first for debugging 41 55 if (!_isInitialized()) { ··· 100 114 return; 101 115 } 102 116 117 + // Increment fetch generation to invalidate any in-flight fetches 118 + _fetchGeneration++; 119 + final currentGeneration = _fetchGeneration; 120 + 103 121 _isLoadingInProgress = true; 104 122 try { 105 123 state = state.copyWith( ··· 112 130 await _maybeFetchNextBatch( 113 131 limit: FeedState.firstLoadLimit, 114 132 replaceExisting: true, 133 + generation: currentGeneration, 115 134 ); 116 135 } catch (e, stackTrace) { 117 - _logger.e('Error in loadAndUpdateFirstLoad: $e', stackTrace: stackTrace); 118 - _lastErrorTime = DateTime.now(); 119 - if (ref.mounted) { 136 + // Only update state if this is still the current generation 137 + if (ref.mounted && _fetchGeneration == currentGeneration) { 138 + _logger.e( 139 + 'Error in loadAndUpdateFirstLoad: $e', 140 + stackTrace: stackTrace, 141 + ); 142 + _lastErrorTime = DateTime.now(); 120 143 state = state.copyWith(loadingFirstLoad: false, error: true); 121 144 } 122 145 } finally { 123 146 _isLoadingInProgress = false; 124 - if (ref.mounted && state.loadingFirstLoad) { 147 + if (ref.mounted && 148 + _fetchGeneration == currentGeneration && 149 + state.loadingFirstLoad) { 125 150 state = state.copyWith(loadingFirstLoad: false); 126 151 } 127 152 } ··· 131 156 List<PostView> posts, { 132 157 String? cursor, 133 158 bool replaceExisting = false, 159 + int? generation, 134 160 }) async { 161 + // Check if generation has changed 162 + if (generation != null && generation != _fetchGeneration) { 163 + _logger.d('Process posts superseded by newer generation'); 164 + return; 165 + } 166 + 135 167 if (posts.isEmpty) { 136 - state = state.copyWith(cursor: cursor, loadingFirstLoad: false); 168 + if (ref.mounted && 169 + (generation == null || generation == _fetchGeneration)) { 170 + state = state.copyWith(cursor: cursor, loadingFirstLoad: false); 171 + } 137 172 return; 138 173 } 139 174 ··· 208 243 209 244 if (!ref.mounted) return; 210 245 246 + // Check generation after async operation 247 + if (generation != null && generation != _fetchGeneration) { 248 + _logger.d('Process posts superseded after filtering'); 249 + return; 250 + } 251 + 211 252 if (filteredPosts.isEmpty) { 212 - state = state.copyWith( 213 - cursor: cursor, 214 - extraInfo: extraInfo, 215 - loadingFirstLoad: false, 216 - ); 253 + if (generation == null || generation == _fetchGeneration) { 254 + state = state.copyWith( 255 + cursor: cursor, 256 + extraInfo: extraInfo, 257 + loadingFirstLoad: false, 258 + ); 259 + } 217 260 return; 218 261 } 219 262 220 263 final updatedPosts = replaceExisting 221 264 ? filteredPosts 222 265 : [...state.loadedPosts, ...filteredPosts]; 223 - state = state.copyWith( 224 - loadedPosts: updatedPosts, 225 - cursor: cursor, 226 - extraInfo: extraInfo, 227 - loadingFirstLoad: false, 228 - ); 266 + if (generation == null || generation == _fetchGeneration) { 267 + state = state.copyWith( 268 + loadedPosts: updatedPosts, 269 + cursor: cursor, 270 + extraInfo: extraInfo, 271 + loadingFirstLoad: false, 272 + ); 273 + } 229 274 230 275 for (final post in filteredPosts) { 231 276 _downloadManager.submitTask( ··· 299 344 Future<void> _maybeFetchNextBatch({ 300 345 int? limit, 301 346 bool replaceExisting = false, 347 + int? generation, 302 348 }) async { 303 349 if (_isFetching || state.isEndOfNetworkFeed) { 304 350 return; ··· 312 358 const maxConsecutiveEmpty = 3; 313 359 while (attempts < maxAttempts && !state.isEndOfNetworkFeed) { 314 360 attempts++; 361 + 362 + // Check if generation has changed (fetch was superseded) 363 + if (generation != null && generation != _fetchGeneration) { 364 + _logger.d('Fetch superseded by newer generation, cancelling'); 365 + return; 366 + } 367 + 315 368 final (:count, :posts, :cursor) = await fetch(limit: limit); 369 + 370 + // Check again after await 371 + if (generation != null && generation != _fetchGeneration) { 372 + _logger.d('Fetch superseded after network call, discarding results'); 373 + return; 374 + } 375 + 316 376 final fetchedCount = count; 317 377 final fetchedPosts = posts; 318 378 if (fetchedPosts.isEmpty) { 319 379 if (fetchedCount == 0 || cursor == null) { 320 380 await endOfNetworkFeed(); 321 - if (ref.mounted) { 381 + if (ref.mounted && 382 + (generation == null || generation == _fetchGeneration)) { 322 383 state = state.copyWith( 323 384 loadingFirstLoad: false, 324 385 isEndOfNetworkFeed: true, ··· 330 391 consecutiveEmptyResults++; 331 392 if (consecutiveEmptyResults >= maxConsecutiveEmpty) { 332 393 await endOfNetworkFeed(); 333 - if (ref.mounted) { 394 + if (ref.mounted && 395 + (generation == null || generation == _fetchGeneration)) { 334 396 state = state.copyWith( 335 397 loadingFirstLoad: false, 336 398 isEndOfNetworkFeed: true, ··· 339 401 break; 340 402 } 341 403 } 342 - if (ref.mounted) { 404 + if (ref.mounted && 405 + (generation == null || generation == _fetchGeneration)) { 343 406 state = state.copyWith(cursor: cursor, loadingFirstLoad: false); 344 407 } 345 408 continue; ··· 351 414 if (newPosts.isEmpty) { 352 415 if (fetchedCount == 0 || cursor == null) { 353 416 await endOfNetworkFeed(); 354 - if (ref.mounted) { 417 + if (ref.mounted && 418 + (generation == null || generation == _fetchGeneration)) { 355 419 state = state.copyWith( 356 420 loadingFirstLoad: false, 357 421 isEndOfNetworkFeed: true, ··· 359 423 } 360 424 break; 361 425 } 362 - if (ref.mounted) { 426 + if (ref.mounted && 427 + (generation == null || generation == _fetchGeneration)) { 363 428 state = state.copyWith(cursor: cursor, loadingFirstLoad: false); 364 429 } 365 430 continue; ··· 369 434 newPosts, 370 435 cursor: cursor, 371 436 replaceExisting: replaceExisting, 437 + generation: generation, 372 438 ); 373 439 break; 374 440 } 375 441 } catch (e, stackTrace) { 376 - _logger.e('Error prefetching feed: $e', stackTrace: stackTrace); 377 - _lastErrorTime = DateTime.now(); 378 - if (ref.mounted) { 442 + // Only update error state if this generation is still current 443 + if (ref.mounted && 444 + (generation == null || generation == _fetchGeneration)) { 445 + _logger.e('Error prefetching feed: $e', stackTrace: stackTrace); 446 + _lastErrorTime = DateTime.now(); 379 447 state = state.copyWith(error: true, loadingFirstLoad: false); 380 448 } 381 449 rethrow;