Harden app for 24-7 offline-first operation
This commit is contained in:
@@ -77,16 +77,75 @@ class ScheduleCacheStatus {
|
||||
class SyncService {
|
||||
SyncService._();
|
||||
static final SyncService instance = SyncService._();
|
||||
static const Duration _autoRefreshCooldown = Duration(hours: 12);
|
||||
|
||||
static Set<String> rollingWindowMonths(DateTime referenceDate) {
|
||||
final currentMonth = DateTime(referenceDate.year, referenceDate.month, 1);
|
||||
final nextMonth = DateTime(referenceDate.year, referenceDate.month + 1, 1);
|
||||
return {
|
||||
DateFormat('yyyy-MM').format(currentMonth),
|
||||
DateFormat('yyyy-MM').format(nextMonth),
|
||||
};
|
||||
}
|
||||
|
||||
static List<String> staleScheduleKeys(
|
||||
Iterable<String> keys,
|
||||
DateTime referenceDate,
|
||||
) {
|
||||
final allowedMonths = rollingWindowMonths(referenceDate);
|
||||
final staleKeys = <String>[];
|
||||
|
||||
for (final key in keys) {
|
||||
final parsed = DateTime.tryParse(key);
|
||||
if (parsed == null) {
|
||||
staleKeys.add(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
final monthKey = DateFormat('yyyy-MM').format(parsed);
|
||||
if (!allowedMonths.contains(monthKey)) {
|
||||
staleKeys.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return staleKeys;
|
||||
}
|
||||
|
||||
Future<void> _pruneScheduleCache(
|
||||
Box<DailyPrayerSchedule> scheduleBox,
|
||||
DateTime referenceDate,
|
||||
) async {
|
||||
final staleKeys = staleScheduleKeys(
|
||||
scheduleBox.keys.cast<String>(),
|
||||
referenceDate,
|
||||
);
|
||||
if (staleKeys.isNotEmpty) {
|
||||
await scheduleBox.deleteAll(staleKeys);
|
||||
await scheduleBox.compact();
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldAttemptAutoRefresh({
|
||||
required ScheduleCacheStatus status,
|
||||
required bool hasTodayData,
|
||||
}) {
|
||||
return !hasTodayData || !status.hasData || status.needsRefreshSoon;
|
||||
}
|
||||
|
||||
DateTime? _parseAttemptTimestamp(String? value) {
|
||||
if (value == null || value.isEmpty) return null;
|
||||
return DateTime.tryParse(value);
|
||||
}
|
||||
|
||||
/// Sync current month + next month prayer data for the configured city.
|
||||
/// Returns true on success.
|
||||
Future<bool> syncMonthlyData() async {
|
||||
Future<bool> syncMonthlyData({DateTime? referenceDate}) async {
|
||||
final settingsBox = Hive.box<AppSettings>(HiveBoxes.settings);
|
||||
final settings = settingsBox.get('default');
|
||||
if (settings == null) return false;
|
||||
|
||||
final cityId = settings.cityIdApi;
|
||||
final now = DateTime.now();
|
||||
final now = referenceDate ?? DateTime.now();
|
||||
final currentMonth = DateFormat('yyyy-MM').format(now);
|
||||
|
||||
// Also fetch next month for continuity
|
||||
@@ -97,10 +156,13 @@ class SyncService {
|
||||
final scheduleBox = Hive.box<DailyPrayerSchedule>(HiveBoxes.prayerSchedule);
|
||||
|
||||
var success = false;
|
||||
var hasCurrentMonth = false;
|
||||
var hasNextMonth = false;
|
||||
|
||||
// Fetch current month
|
||||
final currentData = await api.getMonthlySchedule(cityId, currentMonth);
|
||||
if (currentData.isNotEmpty) {
|
||||
hasCurrentMonth = true;
|
||||
for (final entry in currentData.entries) {
|
||||
final jadwal = entry.value;
|
||||
scheduleBox.put(
|
||||
@@ -124,6 +186,7 @@ class SyncService {
|
||||
// Fetch next month
|
||||
final nextData = await api.getMonthlySchedule(cityId, nextMonth);
|
||||
if (nextData.isNotEmpty) {
|
||||
hasNextMonth = true;
|
||||
for (final entry in nextData.entries) {
|
||||
final jadwal = entry.value;
|
||||
scheduleBox.put(
|
||||
@@ -144,6 +207,9 @@ class SyncService {
|
||||
}
|
||||
|
||||
if (success) {
|
||||
if (hasCurrentMonth && hasNextMonth) {
|
||||
await _pruneScheduleCache(scheduleBox, now);
|
||||
}
|
||||
settings.lastSyncDate = DateFormat('yyyy-MM-dd HH:mm').format(now);
|
||||
await settings.save();
|
||||
}
|
||||
@@ -151,6 +217,38 @@ class SyncService {
|
||||
return success;
|
||||
}
|
||||
|
||||
Future<AutoRefreshResult> autoRefreshIfNeeded({
|
||||
DateTime? referenceDate,
|
||||
}) async {
|
||||
final settingsBox = Hive.box<AppSettings>(HiveBoxes.settings);
|
||||
final settings = settingsBox.get('default');
|
||||
if (settings == null) {
|
||||
return const AutoRefreshResult.skipped('settings-missing');
|
||||
}
|
||||
|
||||
final now = referenceDate ?? DateTime.now();
|
||||
final status = getCacheStatus(now);
|
||||
final hasTodayData = getTodaySchedule(now) != null;
|
||||
|
||||
if (!_shouldAttemptAutoRefresh(status: status, hasTodayData: hasTodayData)) {
|
||||
return const AutoRefreshResult.skipped('cache-fresh');
|
||||
}
|
||||
|
||||
final lastAttempt = _parseAttemptTimestamp(settings.lastAutoSyncAttemptDate);
|
||||
if (lastAttempt != null &&
|
||||
now.difference(lastAttempt) < _autoRefreshCooldown) {
|
||||
return const AutoRefreshResult.skipped('cooldown');
|
||||
}
|
||||
|
||||
settings.lastAutoSyncAttemptDate = now.toIso8601String();
|
||||
await settings.save();
|
||||
|
||||
final synced = await syncMonthlyData(referenceDate: now);
|
||||
return synced
|
||||
? const AutoRefreshResult.synced()
|
||||
: const AutoRefreshResult.failed('sync-failed');
|
||||
}
|
||||
|
||||
/// Get today's prayer schedule from local Hive cache.
|
||||
DailyPrayerSchedule? getTodaySchedule([DateTime? targetDate]) {
|
||||
final scheduleBox =
|
||||
@@ -169,3 +267,24 @@ class SyncService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AutoRefreshResult {
|
||||
final bool attempted;
|
||||
final bool synced;
|
||||
final String reason;
|
||||
|
||||
const AutoRefreshResult._({
|
||||
required this.attempted,
|
||||
required this.synced,
|
||||
required this.reason,
|
||||
});
|
||||
|
||||
const AutoRefreshResult.skipped(String reason)
|
||||
: this._(attempted: false, synced: false, reason: reason);
|
||||
|
||||
const AutoRefreshResult.synced()
|
||||
: this._(attempted: true, synced: true, reason: 'synced');
|
||||
|
||||
const AutoRefreshResult.failed(String reason)
|
||||
: this._(attempted: true, synced: false, reason: reason);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user