Files
plezy/lib/utils/continuation_pagination_coordinator.dart
T
edde746 4307c49cd2 refactor: remove unreachable code paths and unused members
Drops dead code across services, models, utils and widgets, including the
connection auth service, which had no implementer, and the Live TV DVR
provisioning models, which had no caller.

Tests that only covered deleted behaviour are removed or trimmed. No
behaviour change.
2026-07-26 06:09:47 +02:00

159 lines
5.2 KiB
Dart

/// A backend page consumed by [ContinuationPaginationCoordinator].
///
/// [consumedCount] is deliberately separate from [items.length]. A screen may
/// filter or otherwise map wire items while the backend cursor must still
/// advance by the number of records consumed from the response.
class ContinuationPage<T> {
const ContinuationPage({required this.items, required this.totalCount, required this.consumedCount});
final List<T> items;
final int totalCount;
final int consumedCount;
}
enum ContinuationLoadStatus { completed, failed, stale, idle }
typedef ContinuationPageLoader<T> = Future<ContinuationPage<T>> Function(int startIndex);
typedef ContinuationPageHandler<T> = void Function(ContinuationPage<T> page);
/// Coordinates an eager, indexed continuation without depending on Flutter.
///
/// Screens retain ownership of item mapping and presentation through [onPage].
/// This class owns cursor advancement, generation invalidation, retry state,
/// stale-result rejection, and request coalescing.
class ContinuationPaginationCoordinator<T> {
ContinuationPaginationCoordinator({required this.loadPage, required this.onPage, this.onStateChanged, this.onError});
final ContinuationPageLoader<T> loadPage;
final ContinuationPageHandler<T> onPage;
final void Function()? onStateChanged;
final void Function(Object error, StackTrace stackTrace)? onError;
int _generation = 0;
int? _nextStartIndex;
int? _totalCount;
Future<ContinuationLoadStatus>? _inFlight;
int? _inFlightGeneration;
bool _disposed = false;
bool _isLoading = false;
Object? _error;
int? get nextStartIndex => _nextStartIndex;
int? get totalCount => _totalCount;
bool get hasMore => _nextStartIndex != null;
bool get isLoading => _isLoading;
Object? get error => _error;
/// Invalidates all prior work, runs [request], and reports whether its result
/// still belongs to the current generation.
Future<bool> runNewGeneration(Future<void> Function() request) async {
final generation = _beginGeneration();
try {
await request();
} catch (_) {
if (!_isCurrent(generation)) return false;
rethrow;
}
return _isCurrent(generation);
}
/// Seeds the continuation cursor from an accepted initial response.
void setContinuation({required int startIndex, required int totalCount}) {
if (_disposed) return;
_totalCount = totalCount;
_nextStartIndex = startIndex < totalCount ? startIndex : null;
_error = null;
onStateChanged?.call();
}
/// Loads all remaining pages. Calls made while this generation is loading
/// share the same operation and do not issue duplicate backend requests.
Future<ContinuationLoadStatus> loadRemaining() {
final existing = _inFlight;
if (existing != null) return existing;
if (_disposed || _nextStartIndex == null) {
return Future.value(ContinuationLoadStatus.idle);
}
final generation = _generation;
_inFlightGeneration = generation;
final operation = _loadRemaining(generation);
_inFlight = operation;
return operation;
}
/// Retries from the first cursor that did not complete successfully.
Future<ContinuationLoadStatus> retry() => loadRemaining();
/// Invalidates pending results and suppresses future callbacks.
void dispose() {
if (_disposed) return;
_disposed = true;
_generation++;
_nextStartIndex = null;
_totalCount = null;
_inFlight = null;
_inFlightGeneration = null;
_isLoading = false;
_error = null;
}
int _beginGeneration() {
_generation++;
_nextStartIndex = null;
_totalCount = null;
_inFlight = null;
_inFlightGeneration = null;
_isLoading = false;
_error = null;
if (!_disposed) onStateChanged?.call();
return _generation;
}
bool _isCurrent(int generation) => !_disposed && generation == _generation;
Future<ContinuationLoadStatus> _loadRemaining(int generation) async {
_isLoading = true;
_error = null;
onStateChanged?.call();
try {
while (_isCurrent(generation)) {
final startIndex = _nextStartIndex;
if (startIndex == null) return ContinuationLoadStatus.completed;
final page = await loadPage(startIndex);
if (!_isCurrent(generation)) return ContinuationLoadStatus.stale;
if (page.consumedCount <= 0) {
_totalCount = page.totalCount;
_nextStartIndex = null;
onStateChanged?.call();
return ContinuationLoadStatus.completed;
}
onPage(page);
_totalCount = page.totalCount;
final nextStartIndex = startIndex + page.consumedCount;
_nextStartIndex = nextStartIndex < page.totalCount ? nextStartIndex : null;
onStateChanged?.call();
}
return ContinuationLoadStatus.stale;
} catch (exception, stackTrace) {
if (!_isCurrent(generation)) return ContinuationLoadStatus.stale;
_error = exception;
onError?.call(exception, stackTrace);
return ContinuationLoadStatus.failed;
} finally {
if (_isCurrent(generation)) {
_isLoading = false;
onStateChanged?.call();
}
if (_inFlightGeneration == generation) {
_inFlight = null;
_inFlightGeneration = null;
}
}
}
}