mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-15 05:10:28 +02:00
fix: native FLAC handling and extension API optimizations
Native FLAC handling: - Properly detect and publish native FLAC payloads inside MP4 containers - Rename to .flac extension and embed metadata instead of skipping - Fix all code paths: SAF, non-SAF, and native worker finalizer Extension API optimizations: - Enable response compression for API/search calls (faster metadata loads) - Keep downloads uncompressed for accurate progress/streaming - Add separate extensionAPITransport with compression enabled Platform bridge caching: - Cache handleURLWithExtension results (5 min TTL) - Cache customSearchWithExtension results (2 min TTL) - Prevent duplicate in-flight requests for same URL/query Dependency cleanup: - Remove unused sqflite_common_ffi and sqlite3 packages
This commit is contained in:
@@ -543,9 +543,20 @@ object NativeDownloadFinalizer {
|
||||
try {
|
||||
val codec = probePrimaryAudioCodec(localInput, shouldCancel)
|
||||
val isAlreadyNativeFlac = codec == "flac" && isNativeFlacFile(localInput)
|
||||
if (!isLosslessAudioCodec(codec) || isAlreadyNativeFlac) {
|
||||
val suffix = if (isAlreadyNativeFlac) " (native FLAC)" else ""
|
||||
Log.d(TAG, "Preserving native container; audio codec is ${codec.ifBlank { "unknown" }}$suffix")
|
||||
if (!isLosslessAudioCodec(codec)) {
|
||||
Log.d(TAG, "Preserving native container; audio codec is ${codec.ifBlank { "unknown" }}")
|
||||
return
|
||||
}
|
||||
if (isAlreadyNativeFlac) {
|
||||
Log.d(TAG, "Native FLAC payload detected; publishing as FLAC and embedding metadata")
|
||||
val nativeFlacOutput = if (localInput.lowercase(Locale.ROOT).endsWith(".flac")) {
|
||||
localInput
|
||||
} else {
|
||||
File(localInput).copyTo(File(output), overwrite = true).absolutePath
|
||||
}
|
||||
embedBasicMetadata(context, nativeFlacOutput, input, "flac")
|
||||
replaceStatePath(context, input, state, nativeFlacOutput, deleteOld = true)
|
||||
adoptedOutput = true
|
||||
return
|
||||
}
|
||||
val result = runFFmpeg(
|
||||
|
||||
@@ -367,8 +367,8 @@ func newIsolatedExtensionRuntime(ext *loadedExtension) (*goja.Runtime, *extensio
|
||||
jar, _ := newSimpleCookieJar()
|
||||
runtime.cookieJar = jar
|
||||
}
|
||||
runtime.httpClient = newExtensionHTTPClient(ext, runtime.cookieJar, extensionHTTPTimeout(ext, 30*time.Second))
|
||||
runtime.downloadClient = newExtensionHTTPClient(ext, runtime.cookieJar, DownloadTimeout)
|
||||
runtime.httpClient = newExtensionHTTPClient(ext, runtime.cookieJar, extensionHTTPTimeout(ext, 30*time.Second), true)
|
||||
runtime.downloadClient = newExtensionHTTPClient(ext, runtime.cookieJar, DownloadTimeout, false)
|
||||
runtime.RegisterAPIs(vm)
|
||||
runtime.RegisterGoBackendAPIs(vm)
|
||||
|
||||
|
||||
@@ -140,8 +140,8 @@ func newExtensionRuntime(ext *loadedExtension) *extensionRuntime {
|
||||
storageFlushDelay: defaultStorageFlushDelay,
|
||||
}
|
||||
|
||||
runtime.httpClient = newExtensionHTTPClient(ext, jar, extensionHTTPTimeout(ext, 30*time.Second))
|
||||
runtime.downloadClient = newExtensionHTTPClient(ext, jar, DownloadTimeout)
|
||||
runtime.httpClient = newExtensionHTTPClient(ext, jar, extensionHTTPTimeout(ext, 30*time.Second), true)
|
||||
runtime.downloadClient = newExtensionHTTPClient(ext, jar, DownloadTimeout, false)
|
||||
|
||||
return runtime
|
||||
}
|
||||
@@ -247,13 +247,18 @@ func (r *extensionRuntime) bindDownloadCancelContext(req *http.Request) *http.Re
|
||||
return req.WithContext(initDownloadCancel(itemID))
|
||||
}
|
||||
|
||||
func newExtensionHTTPClient(ext *loadedExtension, jar http.CookieJar, timeout time.Duration) *http.Client {
|
||||
func newExtensionHTTPClient(ext *loadedExtension, jar http.CookieJar, timeout time.Duration, compressResponses bool) *http.Client {
|
||||
// Extension sandbox enforces HTTPS-only domains. Do not apply global
|
||||
// allow_http scheme downgrade here, because some extension APIs (e.g.
|
||||
// spotify-web) will redirect http -> https and can end up in 301 loops.
|
||||
// We still reuse sharedTransport so insecure TLS compatibility mode remains effective.
|
||||
// API calls can use response compression for faster metadata/search loads,
|
||||
// while media downloads keep identity transfer semantics for progress/streaming.
|
||||
transport := sharedTransport
|
||||
if compressResponses {
|
||||
transport = extensionAPITransport
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: sharedTransport,
|
||||
Transport: transport,
|
||||
Timeout: timeout,
|
||||
Jar: jar,
|
||||
}
|
||||
|
||||
@@ -79,6 +79,24 @@ var sharedTransport = &http.Transport{
|
||||
DisableCompression: true,
|
||||
}
|
||||
|
||||
var extensionAPITransport = &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 10,
|
||||
MaxConnsPerHost: 20,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
DisableKeepAlives: false,
|
||||
ForceAttemptHTTP2: true,
|
||||
WriteBufferSize: 64 * 1024,
|
||||
ReadBufferSize: 64 * 1024,
|
||||
DisableCompression: false,
|
||||
}
|
||||
|
||||
var metadataTransport = &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
@@ -131,6 +149,7 @@ func GetDownloadClient() *http.Client {
|
||||
|
||||
func CloseIdleConnections() {
|
||||
sharedTransport.CloseIdleConnections()
|
||||
extensionAPITransport.CloseIdleConnections()
|
||||
metadataTransport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
@@ -143,6 +162,7 @@ func SetNetworkCompatibilityOptions(allowHTTP, insecureTLS bool) {
|
||||
networkCompatibilityMu.Unlock()
|
||||
|
||||
applyTLSCompatibility(sharedTransport, insecureTLS)
|
||||
applyTLSCompatibility(extensionAPITransport, insecureTLS)
|
||||
applyTLSCompatibility(metadataTransport, insecureTLS)
|
||||
CloseIdleConnections()
|
||||
|
||||
|
||||
@@ -6320,14 +6320,41 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
final codec = await FFmpegService.probePrimaryAudioCodec(tempPath);
|
||||
final isAlreadyNativeFlac =
|
||||
codec == 'flac' && await FFmpegService.isNativeFlacFile(tempPath);
|
||||
if (!FFmpegService.isLosslessAudioCodec(codec) || isAlreadyNativeFlac) {
|
||||
if (!FFmpegService.isLosslessAudioCodec(codec)) {
|
||||
_log.d(
|
||||
'Preserving native container; audio codec is ${codec ?? 'unknown'}'
|
||||
'${isAlreadyNativeFlac ? ' (native FLAC)' : ''}, '
|
||||
'Preserving native container; audio codec is ${codec ?? 'unknown'}, '
|
||||
'no FLAC container conversion needed.',
|
||||
);
|
||||
return filePath;
|
||||
}
|
||||
if (isAlreadyNativeFlac) {
|
||||
_log.d(
|
||||
'Native FLAC payload detected in temporary container; publishing '
|
||||
'as FLAC and embedding metadata.',
|
||||
);
|
||||
await embedFlacMetadata(tempPath);
|
||||
final rawFileName =
|
||||
(result['file_name'] as String?) ??
|
||||
context.safFileName ??
|
||||
'track';
|
||||
final baseName = rawFileName.replaceFirst(RegExp(r'\.[^.]+$'), '');
|
||||
final newFileName = '$baseName.flac';
|
||||
final newUri = await _writeTempToSaf(
|
||||
treeUri: treeUri,
|
||||
relativeDir: context.safRelativeDir ?? '',
|
||||
fileName: newFileName,
|
||||
mimeType: _mimeTypeForExt('.flac'),
|
||||
srcPath: tempPath,
|
||||
);
|
||||
if (newUri == null) {
|
||||
return null;
|
||||
}
|
||||
if (newUri != filePath) {
|
||||
await _deleteSafFile(filePath);
|
||||
}
|
||||
result['file_name'] = newFileName;
|
||||
return newUri;
|
||||
}
|
||||
flacPath = await FFmpegService.convertM4aToFlac(tempPath);
|
||||
if (flacPath == null) {
|
||||
return null;
|
||||
@@ -6367,14 +6394,26 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
final codec = await FFmpegService.probePrimaryAudioCodec(filePath);
|
||||
final isAlreadyNativeFlac =
|
||||
codec == 'flac' && await FFmpegService.isNativeFlacFile(filePath);
|
||||
if (!FFmpegService.isLosslessAudioCodec(codec) || isAlreadyNativeFlac) {
|
||||
if (!FFmpegService.isLosslessAudioCodec(codec)) {
|
||||
_log.d(
|
||||
'Preserving native container; audio codec is ${codec ?? 'unknown'}'
|
||||
'${isAlreadyNativeFlac ? ' (native FLAC)' : ''}, '
|
||||
'Preserving native container; audio codec is ${codec ?? 'unknown'}, '
|
||||
'no FLAC container conversion needed.',
|
||||
);
|
||||
return filePath;
|
||||
}
|
||||
if (isAlreadyNativeFlac) {
|
||||
var flacPath = filePath;
|
||||
if (!filePath.toLowerCase().endsWith('.flac')) {
|
||||
final renamedPath = filePath.replaceAll(RegExp(r'\.[^.]+$'), '.flac');
|
||||
final targetPath = renamedPath == filePath
|
||||
? '$filePath.flac'
|
||||
: renamedPath;
|
||||
await File(filePath).rename(targetPath);
|
||||
flacPath = targetPath;
|
||||
}
|
||||
await embedFlacMetadata(flacPath);
|
||||
return flacPath;
|
||||
}
|
||||
final flacPath = await FFmpegService.convertM4aToFlac(filePath);
|
||||
if (flacPath == null) {
|
||||
return null;
|
||||
@@ -7721,11 +7760,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
final isAlreadyNativeFlac =
|
||||
codec == 'flac' &&
|
||||
await FFmpegService.isNativeFlacFile(tempPath);
|
||||
if (!FFmpegService.isLosslessAudioCodec(codec) ||
|
||||
isAlreadyNativeFlac) {
|
||||
if (!FFmpegService.isLosslessAudioCodec(codec)) {
|
||||
_log.d(
|
||||
'Preserving native container; audio codec is ${codec ?? 'unknown'}'
|
||||
'${isAlreadyNativeFlac ? ' (native FLAC)' : ''}, '
|
||||
'Preserving native container; audio codec is ${codec ?? 'unknown'}, '
|
||||
'no FLAC container conversion needed.',
|
||||
);
|
||||
final preserveExt = resultOutputExt == '.mp4'
|
||||
@@ -7747,6 +7784,49 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
filePath = newUri;
|
||||
finalSafFileName = newFileName;
|
||||
}
|
||||
} else if (isAlreadyNativeFlac) {
|
||||
_log.d(
|
||||
'Native FLAC payload detected in SAF temp file; '
|
||||
'publishing as FLAC and embedding metadata.',
|
||||
);
|
||||
final finalTrack = _buildTrackForMetadataEmbedding(
|
||||
trackToDownload,
|
||||
result,
|
||||
resolvedAlbumArtist,
|
||||
);
|
||||
|
||||
final backendGenre = result['genre'] as String?;
|
||||
final backendLabel = result['label'] as String?;
|
||||
final backendCopyright = result['copyright'] as String?;
|
||||
|
||||
await _embedMetadataToFile(
|
||||
tempPath,
|
||||
finalTrack,
|
||||
format: 'flac',
|
||||
genre: backendGenre ?? genre,
|
||||
label: backendLabel ?? label,
|
||||
copyright: backendCopyright,
|
||||
downloadService: item.service,
|
||||
writeExternalLrc: false,
|
||||
);
|
||||
|
||||
final newFileName = '${safBaseName ?? 'track'}.flac';
|
||||
final newUri = await _writeTempToSaf(
|
||||
treeUri: settings.downloadTreeUri,
|
||||
relativeDir: effectiveOutputDir,
|
||||
fileName: newFileName,
|
||||
mimeType: _mimeTypeForExt('.flac'),
|
||||
srcPath: tempPath,
|
||||
);
|
||||
if (newUri != null) {
|
||||
if (newUri != currentFilePath) {
|
||||
await _deleteSafFile(currentFilePath);
|
||||
}
|
||||
filePath = newUri;
|
||||
finalSafFileName = newFileName;
|
||||
} else {
|
||||
_log.w('Failed to write native FLAC to SAF');
|
||||
}
|
||||
} else {
|
||||
updateItemStatus(
|
||||
item.id,
|
||||
@@ -7960,13 +8040,49 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
final isAlreadyNativeFlac =
|
||||
codec == 'flac' &&
|
||||
await FFmpegService.isNativeFlacFile(currentFilePath);
|
||||
if (!FFmpegService.isLosslessAudioCodec(codec) ||
|
||||
isAlreadyNativeFlac) {
|
||||
if (!FFmpegService.isLosslessAudioCodec(codec)) {
|
||||
_log.d(
|
||||
'Preserving native container; audio codec is ${codec ?? 'unknown'}'
|
||||
'${isAlreadyNativeFlac ? ' (native FLAC)' : ''}, '
|
||||
'Preserving native container; audio codec is ${codec ?? 'unknown'}, '
|
||||
'no FLAC container conversion needed.',
|
||||
);
|
||||
} else if (isAlreadyNativeFlac) {
|
||||
_log.d(
|
||||
'Native FLAC payload detected; ensuring .flac '
|
||||
'extension and embedding metadata.',
|
||||
);
|
||||
var flacPath = currentFilePath;
|
||||
if (!currentFilePath.toLowerCase().endsWith('.flac')) {
|
||||
final renamedPath = currentFilePath.replaceAll(
|
||||
RegExp(r'\.[^.]+$'),
|
||||
'.flac',
|
||||
);
|
||||
final targetPath = renamedPath == currentFilePath
|
||||
? '$currentFilePath.flac'
|
||||
: renamedPath;
|
||||
await File(currentFilePath).rename(targetPath);
|
||||
flacPath = targetPath;
|
||||
filePath = targetPath;
|
||||
}
|
||||
|
||||
final finalTrack = _buildTrackForMetadataEmbedding(
|
||||
trackToDownload,
|
||||
result,
|
||||
resolvedAlbumArtist,
|
||||
);
|
||||
|
||||
final backendGenre = result['genre'] as String?;
|
||||
final backendLabel = result['label'] as String?;
|
||||
final backendCopyright = result['copyright'] as String?;
|
||||
|
||||
await _embedMetadataToFile(
|
||||
flacPath,
|
||||
finalTrack,
|
||||
format: 'flac',
|
||||
genre: backendGenre ?? genre,
|
||||
label: backendLabel ?? label,
|
||||
copyright: backendCopyright,
|
||||
downloadService: item.service,
|
||||
);
|
||||
} else {
|
||||
updateItemStatus(
|
||||
item.id,
|
||||
|
||||
@@ -21,6 +21,15 @@ class _BridgeCacheEntry {
|
||||
bool get isExpired => DateTime.now().isAfter(expiresAt);
|
||||
}
|
||||
|
||||
class _BridgeListCacheEntry {
|
||||
final List<Map<String, dynamic>> value;
|
||||
final DateTime expiresAt;
|
||||
|
||||
const _BridgeListCacheEntry({required this.value, required this.expiresAt});
|
||||
|
||||
bool get isExpired => DateTime.now().isAfter(expiresAt);
|
||||
}
|
||||
|
||||
class _BridgeInFlight<T> {
|
||||
final String requestId;
|
||||
final String scopeKey;
|
||||
@@ -39,6 +48,8 @@ class PlatformBridge {
|
||||
static const _backgroundJsonDecodeThresholdBytes = 128 * 1024;
|
||||
static const _metadataCacheTtl = Duration(minutes: 20);
|
||||
static const _availabilityCacheTtl = Duration(minutes: 15);
|
||||
static const _urlHandleCacheTtl = Duration(minutes: 5);
|
||||
static const _customSearchCacheTtl = Duration(minutes: 2);
|
||||
static const _bridgeCacheMaxEntries = 256;
|
||||
static const _metadataPersistentCacheKey = 'bridge_metadata_lookup_cache_v1';
|
||||
static const _availabilityPersistentCacheKey =
|
||||
@@ -51,9 +62,13 @@ class PlatformBridge {
|
||||
);
|
||||
static final Map<String, _BridgeCacheEntry> _metadataCache = {};
|
||||
static final Map<String, _BridgeCacheEntry> _availabilityCache = {};
|
||||
static final Map<String, _BridgeCacheEntry> _urlHandleCache = {};
|
||||
static final Map<String, _BridgeListCacheEntry> _customSearchCache = {};
|
||||
static final Map<String, Future<Map<String, dynamic>>> _metadataInFlight = {};
|
||||
static final Map<String, Future<Map<String, dynamic>>> _availabilityInFlight =
|
||||
{};
|
||||
static final Map<String, Future<Map<String, dynamic>?>> _urlHandleInFlight =
|
||||
{};
|
||||
static final Map<String, _BridgeInFlight<List<Map<String, dynamic>>>>
|
||||
_customSearchInFlight = {};
|
||||
static final Map<String, _BridgeInFlight<Map<String, dynamic>?>>
|
||||
@@ -329,8 +344,11 @@ class PlatformBridge {
|
||||
_persistentLookupCacheLoadFuture = null;
|
||||
_metadataCache.clear();
|
||||
_availabilityCache.clear();
|
||||
_urlHandleCache.clear();
|
||||
_customSearchCache.clear();
|
||||
_metadataInFlight.clear();
|
||||
_availabilityInFlight.clear();
|
||||
_urlHandleInFlight.clear();
|
||||
for (final inFlight in _customSearchInFlight.values) {
|
||||
_cancelExtensionRequestUnawaited(inFlight.requestId);
|
||||
}
|
||||
@@ -1388,7 +1406,11 @@ class PlatformBridge {
|
||||
_cancelCustomSearchInFlightForScope(scopeKey, exceptKey: cacheKey);
|
||||
}
|
||||
|
||||
final cached = _getCachedMapList(_customSearchCache, cacheKey);
|
||||
if (cached != null) return cached;
|
||||
|
||||
final requestId = _nextExtensionRequestId('customSearch', extensionId);
|
||||
final generation = _lookupCacheGeneration;
|
||||
final future = (() async {
|
||||
final result = await _channel.invokeMethod('customSearchWithExtension', {
|
||||
'extension_id': extensionId,
|
||||
@@ -1396,7 +1418,16 @@ class PlatformBridge {
|
||||
'options': optionsJson,
|
||||
'request_id': requestId,
|
||||
});
|
||||
return _decodeMapListResult(result, 'customSearchWithExtension');
|
||||
final decoded = _decodeMapListResult(result, 'customSearchWithExtension');
|
||||
if (generation == _lookupCacheGeneration) {
|
||||
_putMemoryCachedMapList(
|
||||
_customSearchCache,
|
||||
cacheKey,
|
||||
decoded,
|
||||
_customSearchCacheTtl,
|
||||
);
|
||||
}
|
||||
return decoded;
|
||||
})();
|
||||
|
||||
final entry = _BridgeInFlight<List<Map<String, dynamic>>>(
|
||||
@@ -1422,14 +1453,115 @@ class PlatformBridge {
|
||||
static Future<Map<String, dynamic>?> handleURLWithExtension(
|
||||
String url,
|
||||
) async {
|
||||
final cacheKey = url.trim();
|
||||
if (cacheKey.isEmpty) return null;
|
||||
|
||||
final cached = _getCachedMap(_urlHandleCache, cacheKey);
|
||||
if (cached != null) return cached;
|
||||
|
||||
final inFlight = _urlHandleInFlight[cacheKey];
|
||||
if (inFlight != null) return _copyNullableStringMap(await inFlight);
|
||||
|
||||
final generation = _lookupCacheGeneration;
|
||||
final future = (() async {
|
||||
try {
|
||||
final result = await _channel.invokeMethod('handleURLWithExtension', {
|
||||
'url': url,
|
||||
});
|
||||
final decoded = _decodeNullableMapResult(
|
||||
result,
|
||||
'handleURLWithExtension',
|
||||
);
|
||||
if (generation == _lookupCacheGeneration &&
|
||||
decoded != null &&
|
||||
_isCacheableURLHandleResult(decoded)) {
|
||||
_putMemoryCachedMap(
|
||||
_urlHandleCache,
|
||||
cacheKey,
|
||||
decoded,
|
||||
_urlHandleCacheTtl,
|
||||
);
|
||||
}
|
||||
return decoded;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
_urlHandleInFlight[cacheKey] = future;
|
||||
try {
|
||||
final result = await _channel.invokeMethod('handleURLWithExtension', {
|
||||
'url': url,
|
||||
});
|
||||
return _decodeNullableMapResult(result, 'handleURLWithExtension');
|
||||
} catch (e) {
|
||||
return _copyNullableStringMap(await future);
|
||||
} finally {
|
||||
if (identical(_urlHandleInFlight[cacheKey], future)) {
|
||||
_urlHandleInFlight.remove(cacheKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool _isCacheableURLHandleResult(Map<String, dynamic> result) {
|
||||
final type = result['type']?.toString();
|
||||
if (type == null || type.isEmpty) return false;
|
||||
if (type == 'track') {
|
||||
final track = result['track'];
|
||||
if (track is! Map) return false;
|
||||
final name = track['name']?.toString().trim() ?? '';
|
||||
return name.isNotEmpty;
|
||||
}
|
||||
return type == 'album' || type == 'playlist' || type == 'artist';
|
||||
}
|
||||
|
||||
static void _putMemoryCachedMap(
|
||||
Map<String, _BridgeCacheEntry> cache,
|
||||
String key,
|
||||
Map<String, dynamic> value,
|
||||
Duration ttl,
|
||||
) {
|
||||
_pruneExpiredBridgeCache(cache);
|
||||
while (cache.length >= _bridgeCacheMaxEntries && cache.isNotEmpty) {
|
||||
cache.remove(cache.keys.first);
|
||||
}
|
||||
cache[key] = _BridgeCacheEntry(
|
||||
value: _copyStringMap(value),
|
||||
expiresAt: DateTime.now().add(ttl),
|
||||
);
|
||||
}
|
||||
|
||||
static List<Map<String, dynamic>>? _getCachedMapList(
|
||||
Map<String, _BridgeListCacheEntry> cache,
|
||||
String key,
|
||||
) {
|
||||
_pruneExpiredBridgeListCache(cache);
|
||||
final entry = cache[key];
|
||||
if (entry == null) return null;
|
||||
if (entry.isExpired) {
|
||||
cache.remove(key);
|
||||
return null;
|
||||
}
|
||||
return _copyMapList(entry.value);
|
||||
}
|
||||
|
||||
static void _putMemoryCachedMapList(
|
||||
Map<String, _BridgeListCacheEntry> cache,
|
||||
String key,
|
||||
List<Map<String, dynamic>> value,
|
||||
Duration ttl,
|
||||
) {
|
||||
_pruneExpiredBridgeListCache(cache);
|
||||
while (cache.length >= _bridgeCacheMaxEntries && cache.isNotEmpty) {
|
||||
cache.remove(cache.keys.first);
|
||||
}
|
||||
cache[key] = _BridgeListCacheEntry(
|
||||
value: _copyMapList(value),
|
||||
expiresAt: DateTime.now().add(ttl),
|
||||
);
|
||||
}
|
||||
|
||||
static void _pruneExpiredBridgeListCache(
|
||||
Map<String, _BridgeListCacheEntry> cache,
|
||||
) {
|
||||
if (cache.isEmpty) return;
|
||||
final now = DateTime.now();
|
||||
cache.removeWhere((_, entry) => now.isAfter(entry.expiresAt));
|
||||
}
|
||||
|
||||
static Future<String?> findURLHandler(String url) async {
|
||||
|
||||
@@ -1170,14 +1170,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.6+1"
|
||||
sqflite_common_ffi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite_common_ffi
|
||||
sha256: cd0c7f7de39a08f2d54ef144d9058c46eca8461879aaa648025643455c1e5a20
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0+3"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1194,14 +1186,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: caa693ad15a587a2b4fde093b728131a1827903872171089dedb16f7665d3a91
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user