85 lines
2.2 KiB
Dart
85 lines
2.2 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:relationship_saver/features/sync/sync_coordinator.dart';
|
|
|
|
typedef SyncNowRunner = Future<SyncRunResult> Function();
|
|
typedef TriggerGate = Future<bool> Function();
|
|
|
|
/// Coordinates auto-triggered sync calls with cooldown and concurrency guards.
|
|
class SyncAutoTriggerController {
|
|
SyncAutoTriggerController({
|
|
required SyncNowRunner syncNow,
|
|
required DateTime Function() now,
|
|
TriggerGate? canTrigger,
|
|
this.minimumInterval = const Duration(minutes: 3),
|
|
}) : _syncNow = syncNow,
|
|
_now = now,
|
|
_canTrigger = canTrigger ?? _alwaysAllowed;
|
|
|
|
final SyncNowRunner _syncNow;
|
|
final DateTime Function() _now;
|
|
final TriggerGate _canTrigger;
|
|
final Duration minimumInterval;
|
|
|
|
bool _running = false;
|
|
DateTime? _lastTriggeredAt;
|
|
int _consecutiveFailures = 0;
|
|
|
|
@visibleForTesting
|
|
bool get isRunning => _running;
|
|
|
|
@visibleForTesting
|
|
DateTime? get lastTriggeredAt => _lastTriggeredAt;
|
|
|
|
@visibleForTesting
|
|
int get consecutiveFailures => _consecutiveFailures;
|
|
|
|
/// Triggers `syncNow` when not already running and outside cooldown window.
|
|
Future<bool> trigger({bool ignoreCooldown = false}) async {
|
|
if (_running) {
|
|
return false;
|
|
}
|
|
|
|
final DateTime now = _now();
|
|
final DateTime? last = _lastTriggeredAt;
|
|
if (!ignoreCooldown &&
|
|
last != null &&
|
|
now.difference(last) < _cooldownForCurrentState()) {
|
|
return false;
|
|
}
|
|
|
|
if (!await _canTrigger()) {
|
|
return false;
|
|
}
|
|
|
|
_running = true;
|
|
_lastTriggeredAt = now;
|
|
try {
|
|
final SyncRunResult result = await _syncNow();
|
|
if (result.success) {
|
|
_consecutiveFailures = 0;
|
|
} else {
|
|
_consecutiveFailures += 1;
|
|
}
|
|
return true;
|
|
} catch (_) {
|
|
_consecutiveFailures += 1;
|
|
return true;
|
|
} finally {
|
|
_running = false;
|
|
}
|
|
}
|
|
|
|
Duration _cooldownForCurrentState() {
|
|
if (_consecutiveFailures <= 0) {
|
|
return minimumInterval;
|
|
}
|
|
final int cappedFailures = math.min(_consecutiveFailures, 4);
|
|
final int multiplier = 1 << cappedFailures;
|
|
return minimumInterval * multiplier;
|
|
}
|
|
|
|
static Future<bool> _alwaysAllowed() async => true;
|
|
}
|