mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-02 11:05:38 +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:
@@ -59,6 +59,7 @@ func CheckExtensionHealthJSON(extensionID string) (string, error) {
|
||||
}
|
||||
|
||||
result := CheckExtensionHealth(ext)
|
||||
cacheExtensionHealthResult(ext, result)
|
||||
bytes, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -86,6 +87,20 @@ func CheckExtensionHealthCached(ext *loadedExtension) ExtensionHealthResult {
|
||||
extensionHealthCacheMu.Unlock()
|
||||
|
||||
result := CheckExtensionHealth(ext)
|
||||
cacheExtensionHealthResult(ext, result)
|
||||
return result
|
||||
}
|
||||
|
||||
func cacheExtensionHealthResult(ext *loadedExtension, result ExtensionHealthResult) {
|
||||
if ext == nil || ext.Manifest == nil || len(ext.Manifest.ServiceHealth) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cacheKey := strings.TrimSpace(ext.ID)
|
||||
if cacheKey == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ttl := extensionHealthCacheTTL(ext.Manifest.ServiceHealth)
|
||||
if result.Status == "unknown" && ttl > extensionHealthUnknownCache {
|
||||
ttl = extensionHealthUnknownCache
|
||||
@@ -94,11 +109,9 @@ func CheckExtensionHealthCached(ext *loadedExtension) ExtensionHealthResult {
|
||||
extensionHealthCacheMu.Lock()
|
||||
extensionHealthCache[cacheKey] = cachedExtensionHealthResult{
|
||||
result: result,
|
||||
expiresAt: now.Add(ttl),
|
||||
expiresAt: time.Now().Add(ttl),
|
||||
}
|
||||
extensionHealthCacheMu.Unlock()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func CheckExtensionHealth(ext *loadedExtension) ExtensionHealthResult {
|
||||
@@ -271,7 +284,7 @@ func runExtensionHealthCheck(manifest *ExtensionManifest, check ExtensionHealthC
|
||||
}
|
||||
|
||||
func isTransientExtensionHealthError(err error) bool {
|
||||
return isTransientNetworkError(err)
|
||||
return isTransientNetworkError(err) || isConnectivityFailure(err)
|
||||
}
|
||||
|
||||
func classifyExtensionHealthBody(body []byte, serviceKey string) (string, string) {
|
||||
|
||||
@@ -32,8 +32,8 @@ func TestExtensionHealthClassificationAndValidation(t *testing.T) {
|
||||
if !isTransientExtensionHealthError(context.DeadlineExceeded) || !isTransientExtensionHealthError(&net.DNSError{IsTimeout: true}) {
|
||||
t.Fatal("expected timeout health errors to be transient")
|
||||
}
|
||||
if isTransientExtensionHealthError(&net.DNSError{IsNotFound: true}) {
|
||||
t.Fatal("expected non-timeout DNS errors to be non-transient")
|
||||
if !isTransientExtensionHealthError(&net.DNSError{IsNotFound: true}) {
|
||||
t.Fatal("expected health transport lookup errors to be indeterminate")
|
||||
}
|
||||
|
||||
if result := CheckExtensionHealth(nil); result.Status != "offline" {
|
||||
|
||||
@@ -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