fix(runtime): bound resources and quarantine stalled extensions

This commit is contained in:
zarzet
2026-08-26 21:09:05 +07:00
parent 0e3fca9967
commit 747dea886c
18 changed files with 402 additions and 35 deletions
+4 -1
View File
@@ -32,7 +32,10 @@ import 'package:spotiflac_android/widgets/selection_action_button.dart';
import 'package:spotiflac_android/widgets/selection_bottom_bar.dart';
class _AlbumCache {
static final _cache = TtlCache<List<Track>>(const Duration(minutes: 10));
static final _cache = TtlCache<List<Track>>(
const Duration(minutes: 10),
maxEntries: 40,
);
static List<Track>? get(String albumId) => _cache.get(albumId);
+4 -1
View File
@@ -39,7 +39,10 @@ import 'package:spotiflac_android/widgets/view_queue_snackbar_action.dart';
part 'artist_screen_widgets.dart';
class _ArtistCache {
static final _cache = TtlCache<_CacheEntry>(const Duration(minutes: 10));
static final _cache = TtlCache<_CacheEntry>(
const Duration(minutes: 10),
maxEntries: 24,
);
static _CacheEntry? get(String artistId) => _cache.get(artistId);
@@ -21,13 +21,14 @@ class _EmbeddedCoverCacheEntry {
/// when the source file changed.
class DownloadedEmbeddedCoverResolver {
static const int _maxCacheEntries = 180;
static const int _maxFailedExtractEntries = 360;
static final LinkedHashMap<String, _EmbeddedCoverCacheEntry> _cache =
LinkedHashMap<String, _EmbeddedCoverCacheEntry>();
static final Set<String> _pendingExtract = <String>{};
static final Set<String> _pendingRefresh = <String>{};
static final Set<String> _pendingPreviewValidation = <String>{};
static final Set<String> _failedExtract = <String>{};
static final LinkedHashSet<String> _failedExtract = LinkedHashSet<String>();
static String cleanFilePath(String? filePath) {
if (filePath == null) return '';
@@ -132,6 +133,15 @@ class DownloadedEmbeddedCoverResolver {
}
}
static void _rememberFailedExtract(String cleanPath) {
_failedExtract
..remove(cleanPath)
..add(cleanPath);
while (_failedExtract.length > _maxFailedExtractEntries) {
_failedExtract.remove(_failedExtract.first);
}
}
static void _validateCachedPreviewAsync(
String cleanPath,
_EmbeddedCoverCacheEntry entry, {
@@ -186,7 +196,7 @@ class DownloadedEmbeddedCoverResolver {
final hasCover =
result['error'] == null && await File(outputPath).exists();
if (!hasCover) {
_failedExtract.add(cleanPath);
_rememberFailedExtract(cleanPath);
_scheduleTempCoverCleanup(outputPath);
return;
}
@@ -205,7 +215,7 @@ class DownloadedEmbeddedCoverResolver {
}
onChanged?.call();
} catch (_) {
_failedExtract.add(cleanPath);
_rememberFailedExtract(cleanPath);
_scheduleTempCoverCleanup(outputPath);
} finally {
_pendingExtract.remove(cleanPath);
+41 -7
View File
@@ -1,22 +1,56 @@
/// Simple in-memory cache where each entry expires after [ttl].
import 'dart:collection';
/// Bounded in-memory LRU cache where each entry expires after [ttl].
class TtlCache<T> {
final Duration ttl;
final Map<String, _TtlEntry<T>> _entries = {};
final int maxEntries;
final LinkedHashMap<String, _TtlEntry<T>> _entries =
LinkedHashMap<String, _TtlEntry<T>>();
TtlCache(this.ttl);
TtlCache(this.ttl, {this.maxEntries = 100}) {
if (maxEntries <= 0) {
throw ArgumentError.value(
maxEntries,
'maxEntries',
'must be greater than zero',
);
}
}
T? get(String key) {
_removeExpired();
final entry = _entries[key];
if (entry == null) return null;
if (DateTime.now().isAfter(entry.expiresAt)) {
_entries.remove(key);
return null;
}
// LinkedHashMap preserves insertion order, so reinserting makes this the
// most recently used entry.
_entries
..remove(key)
..[key] = entry;
return entry.value;
}
void set(String key, T value) {
_removeExpired();
_entries.remove(key);
_entries[key] = _TtlEntry(value, DateTime.now().add(ttl));
while (_entries.length > maxEntries) {
_entries.remove(_entries.keys.first);
}
}
void remove(String key) => _entries.remove(key);
void clear() => _entries.clear();
int get length {
_removeExpired();
return _entries.length;
}
void _removeExpired() {
if (_entries.isEmpty) return;
final now = DateTime.now();
_entries.removeWhere((_, entry) => !now.isBefore(entry.expiresAt));
}
}
+5 -1
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:spotiflac_android/widgets/app_bottom_sheet.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -36,19 +38,21 @@ class DuplicateReviewSheet extends ConsumerStatefulWidget {
}
class _DuplicateReviewSheetState extends ConsumerState<DuplicateReviewSheet> {
late final LocalLibraryNotifier _localLibraryNotifier;
late Future<List<IsrcDuplicateGroup>> _groupsFuture;
bool _deletedLocalRows = false;
@override
void initState() {
super.initState();
_localLibraryNotifier = ref.read(localLibraryProvider.notifier);
_groupsFuture = LibraryDatabase.instance.findIsrcDuplicateGroups();
}
@override
void dispose() {
if (_deletedLocalRows) {
ref.read(localLibraryProvider.notifier).reloadFromStorage();
unawaited(_localLibraryNotifier.reloadFromStorage());
}
super.dispose();
}