Files
SpotiFLAC-Mobile/lib/providers/download_verification_retry_guard.dart
T

139 lines
4.0 KiB
Dart

import 'dart:async';
class DownloadVerificationRetryGuard {
final Set<String> _grantedRetryKeys = {};
bool hasRetriedAfterGrant(String itemId, String service) =>
_grantedRetryKeys.contains(_key(itemId, service));
void recordVerificationResult(
String itemId,
String service, {
required bool granted,
}) {
if (granted) {
_grantedRetryKeys.add(_key(itemId, service));
}
}
void clearItem(String itemId) {
_grantedRetryKeys.removeWhere(
(retryKey) => retryKey == itemId || retryKey.startsWith('$itemId::'),
);
}
void retainItems(Set<String> itemIds) {
_grantedRetryKeys.removeWhere((retryKey) {
final itemId = retryKey.split('::').first;
return !itemIds.contains(itemId);
});
}
String _key(String itemId, String service) =>
'$itemId::${service.trim().toLowerCase()}';
}
/// Shares one verification challenge between downloads for the same service
/// while still allowing each queue item to stop waiting independently.
///
/// When the last waiter leaves, [startFlow]'s cancellation signal completes so
/// browser timers and grant subscriptions can be released immediately.
class DownloadVerificationWaitCoordinator {
final Map<String, _ActiveDownloadVerificationFlow> _flowsByService = {};
final Map<String, Completer<void>> _cancellationsByItem = {};
bool hasActiveFlow(String service) =>
_flowsByService.containsKey(_serviceKey(service));
Future<bool> waitForGrant({
required String itemId,
required String service,
required Future<bool> Function(Future<void> cancellationSignal) startFlow,
}) {
final previousCancellation = _cancellationsByItem[itemId];
if (previousCancellation != null && !previousCancellation.isCompleted) {
previousCancellation.complete();
}
final itemCancellation = Completer<void>();
_cancellationsByItem[itemId] = itemCancellation;
final serviceKey = _serviceKey(service);
var activeFlow = _flowsByService[serviceKey];
if (activeFlow == null) {
final flowCancellation = Completer<void>();
final result = Future<bool>.sync(
() => startFlow(flowCancellation.future),
);
activeFlow = _ActiveDownloadVerificationFlow(
result: result,
cancellation: flowCancellation,
);
_flowsByService[serviceKey] = activeFlow;
}
final waiter = Object();
activeFlow.waiters.add(waiter);
return _waitForGrant(
itemId: itemId,
serviceKey: serviceKey,
activeFlow: activeFlow,
waiter: waiter,
itemCancellation: itemCancellation,
);
}
Future<bool> _waitForGrant({
required String itemId,
required String serviceKey,
required _ActiveDownloadVerificationFlow activeFlow,
required Object waiter,
required Completer<void> itemCancellation,
}) async {
try {
return await Future.any<bool>([
activeFlow.result,
itemCancellation.future.then((_) => false),
]);
} finally {
if (identical(_cancellationsByItem[itemId], itemCancellation)) {
_cancellationsByItem.remove(itemId);
}
activeFlow.waiters.remove(waiter);
if (activeFlow.waiters.isEmpty &&
identical(_flowsByService[serviceKey], activeFlow)) {
_flowsByService.remove(serviceKey);
if (!activeFlow.cancellation.isCompleted) {
activeFlow.cancellation.complete();
}
}
}
}
void cancelItem(String itemId) {
final cancellation = _cancellationsByItem[itemId];
if (cancellation != null && !cancellation.isCompleted) {
cancellation.complete();
}
}
void cancelAll() {
for (final cancellation in _cancellationsByItem.values.toList()) {
if (!cancellation.isCompleted) cancellation.complete();
}
}
String _serviceKey(String service) => service.trim().toLowerCase();
}
class _ActiveDownloadVerificationFlow {
_ActiveDownloadVerificationFlow({
required this.result,
required this.cancellation,
});
final Future<bool> result;
final Completer<void> cancellation;
final Set<Object> waiters = {};
}