diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index d2a32526..a0927d06 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -161,7 +161,8 @@ class TrackNotifier extends Notifier { return const TrackState(); } - bool _isRequestValid(int requestId) => requestId == _currentRequestId; + bool _isRequestValid(int requestId) => + ref.mounted && requestId == _currentRequestId; Future fetchFromUrl(String url) async { final requestId = ++_currentRequestId; @@ -170,6 +171,7 @@ class TrackNotifier extends Notifier { try { var extensionHandler = await PlatformBridge.findURLHandler(url); + if (!_isRequestValid(requestId)) return; if (extensionHandler == null) { final extensionState = ref.read(extensionProvider); if (!extensionState.isInitialized && extensionState.isLoading) { @@ -181,6 +183,7 @@ class TrackNotifier extends Notifier { .waitForInitialization(timeout: _extensionInitRetryTimeout); if (!_isRequestValid(requestId)) return; extensionHandler = await PlatformBridge.findURLHandler(url); + if (!_isRequestValid(requestId)) return; } } @@ -217,6 +220,7 @@ class TrackNotifier extends Notifier { if (attempt < 3) { await Future.delayed(const Duration(milliseconds: 500)); + if (!_isRequestValid(requestId)) return; } } @@ -547,6 +551,7 @@ class TrackNotifier extends Notifier { } void clear() { + _currentRequestId++; state = const TrackState(); } diff --git a/test/live_search_cancellation_test.dart b/test/live_search_cancellation_test.dart index ad3e6c1a..5ae79d08 100644 --- a/test/live_search_cancellation_test.dart +++ b/test/live_search_cancellation_test.dart @@ -62,4 +62,68 @@ void main() { expect(container.read(trackProvider).isLoading, isFalse); }, ); + + for (final fails in [false, true]) { + test( + 'clear rejects a pending search ${fails ? 'error' : 'result'}', + () async { + const channel = MethodChannel('com.zarz.spotiflac/backend'); + final messenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + final response = Completer(); + final started = Completer(); + messenger.setMockMethodCallHandler(channel, (call) async { + if (call.method != 'customSearchWithExtension') return null; + started.complete(); + return response.future; + }); + addTearDown(() => messenger.setMockMethodCallHandler(channel, null)); + final container = ProviderContainer(); + addTearDown(container.dispose); + final notifier = container.read(trackProvider.notifier); + final search = notifier.customSearch('clear-search-$fails', 'query'); + await started.future; + notifier.clear(); + if (fails) { + response.completeError(PlatformException(code: 'late-search-error')); + } else { + response.complete('[{"id":"late","name":"Late result"}]'); + } + await search; + final state = container.read(trackProvider); + expect(state.hasContent, isFalse); + expect(state.isLoading, isFalse); + expect(state.error, isNull); + }, + ); + } + + for (final handler in [null, 'clear-url-provider']) { + test('clear rejects URL preflight completion ($handler)', () async { + const channel = MethodChannel('com.zarz.spotiflac/backend'); + final messenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + final response = Completer(); + final started = Completer(); + messenger.setMockMethodCallHandler(channel, (call) async { + expect(call.method, 'findURLHandler'); + started.complete(); + return response.future; + }); + addTearDown(() => messenger.setMockMethodCallHandler(channel, null)); + final container = ProviderContainer(); + addTearDown(container.dispose); + final notifier = container.read(trackProvider.notifier); + final fetch = notifier.fetchFromUrl( + 'https://example.test/clear-$handler', + ); + await started.future; + notifier.clear(); + response.complete(handler); + await fetch; + expect(container.read(trackProvider).hasContent, isFalse); + expect(container.read(trackProvider).isLoading, isFalse); + expect(container.read(trackProvider).error, isNull); + }); + } }