mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-17 00:20:32 +02:00
feat(extension-health): cache results, force refresh, treat lookup errors as transient
Cache health results after each check, add a force flag to bypass the cache when the download picker opens or a service is selected, guard stale concurrent results via a per-extension request serial, and treat DNS lookup failures as indeterminate/transient.
This commit is contained in:
@@ -814,6 +814,7 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
Completer<void>? _initializationCompleter;
|
||||
final Map<String, DateTime> _healthExpiresAt = {};
|
||||
final Map<String, Future<ExtensionHealthStatus?>> _healthInFlight = {};
|
||||
final Map<String, int> _healthRequestSerial = {};
|
||||
|
||||
@override
|
||||
ExtensionState build() {
|
||||
@@ -825,6 +826,7 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
_appLifecycleListener = null;
|
||||
_healthExpiresAt.clear();
|
||||
_healthInFlight.clear();
|
||||
_healthRequestSerial.clear();
|
||||
});
|
||||
return const ExtensionState();
|
||||
}
|
||||
@@ -954,15 +956,18 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
}
|
||||
|
||||
void _scheduleExtensionHealthRefresh(List<Extension> extensions) {
|
||||
void _scheduleExtensionHealthRefresh(
|
||||
List<Extension> extensions, {
|
||||
bool force = false,
|
||||
}) {
|
||||
for (final ext in extensions) {
|
||||
if (!ext.enabled || !ext.hasServiceHealth) continue;
|
||||
unawaited(checkExtensionHealth(ext.id));
|
||||
unawaited(checkExtensionHealth(ext.id, force: force));
|
||||
}
|
||||
}
|
||||
|
||||
void refreshEnabledExtensionHealth() {
|
||||
_scheduleExtensionHealthRefresh(state.extensions);
|
||||
void refreshEnabledExtensionHealth({bool force = false}) {
|
||||
_scheduleExtensionHealthRefresh(state.extensions, force: force);
|
||||
}
|
||||
|
||||
Future<ExtensionHealthStatus?> checkExtensionHealth(
|
||||
@@ -990,17 +995,22 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
final requestSerial = (_healthRequestSerial[extensionId] ?? 0) + 1;
|
||||
_healthRequestSerial[extensionId] = requestSerial;
|
||||
|
||||
final future = () async {
|
||||
try {
|
||||
final result = await PlatformBridge.checkExtensionHealth(extensionId);
|
||||
final status = ExtensionHealthStatus.fromJson(result);
|
||||
final updated = Map<String, ExtensionHealthStatus>.of(
|
||||
state.healthStatuses,
|
||||
)..[extensionId] = status;
|
||||
_healthExpiresAt[extensionId] = DateTime.now().add(
|
||||
_extensionHealthCacheTtl,
|
||||
);
|
||||
state = state.copyWith(healthStatuses: updated);
|
||||
if (_healthRequestSerial[extensionId] == requestSerial) {
|
||||
final updated = Map<String, ExtensionHealthStatus>.of(
|
||||
state.healthStatuses,
|
||||
)..[extensionId] = status;
|
||||
_healthExpiresAt[extensionId] = DateTime.now().add(
|
||||
_extensionHealthCacheTtl,
|
||||
);
|
||||
state = state.copyWith(healthStatuses: updated);
|
||||
}
|
||||
return status;
|
||||
} catch (e) {
|
||||
_log.w('Failed to check extension health for $extensionId: $e');
|
||||
@@ -1010,16 +1020,20 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
checkedAt: DateTime.now(),
|
||||
checks: const [],
|
||||
);
|
||||
final updated = Map<String, ExtensionHealthStatus>.of(
|
||||
state.healthStatuses,
|
||||
)..[extensionId] = status;
|
||||
_healthExpiresAt[extensionId] = DateTime.now().add(
|
||||
const Duration(seconds: 20),
|
||||
);
|
||||
state = state.copyWith(healthStatuses: updated);
|
||||
if (_healthRequestSerial[extensionId] == requestSerial) {
|
||||
final updated = Map<String, ExtensionHealthStatus>.of(
|
||||
state.healthStatuses,
|
||||
)..[extensionId] = status;
|
||||
_healthExpiresAt[extensionId] = DateTime.now().add(
|
||||
const Duration(seconds: 20),
|
||||
);
|
||||
state = state.copyWith(healthStatuses: updated);
|
||||
}
|
||||
return status;
|
||||
} finally {
|
||||
_healthInFlight.remove(extensionId);
|
||||
if (_healthRequestSerial[extensionId] == requestSerial) {
|
||||
_healthInFlight.remove(extensionId);
|
||||
}
|
||||
}
|
||||
}();
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -74,7 +75,9 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
ref.read(extensionProvider.notifier).refreshEnabledExtensionHealth();
|
||||
ref
|
||||
.read(extensionProvider.notifier)
|
||||
.refreshEnabledExtensionHealth(force: true);
|
||||
});
|
||||
final downloadExtensions = _downloadExtensions();
|
||||
final recommended = widget.recommendedService;
|
||||
@@ -102,6 +105,17 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
|
||||
return const [];
|
||||
}
|
||||
|
||||
void _selectService(Extension extension) {
|
||||
setState(() => _selectedService = extension.id);
|
||||
if (extension.hasServiceHealth) {
|
||||
unawaited(
|
||||
ref
|
||||
.read(extensionProvider.notifier)
|
||||
.checkExtensionHealth(extension.id, force: true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
@@ -166,8 +180,7 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
|
||||
? extensionState.healthStatuses[ext.id]?.status
|
||||
: null,
|
||||
isSelected: _selectedService == ext.id,
|
||||
onTap: () =>
|
||||
setState(() => _selectedService = ext.id),
|
||||
onTap: () => _selectService(ext),
|
||||
iconPath: ext.iconPath,
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user