fix(offline-first): harden local schedule cache lookup and key normalization
This commit is contained in:
@@ -26,6 +26,26 @@ class ScheduleCacheStatus {
|
||||
bool get hasData => startDate != null && endDate != null && cachedDays > 0;
|
||||
bool get isExpired => hasData && daysUntilRefresh < 0;
|
||||
bool get needsRefreshSoon => hasData && daysUntilRefresh <= 3;
|
||||
static final RegExp _flexDateKeyRegex = RegExp(r'^(\d{4})-(\d{1,2})-(\d{1,2})$');
|
||||
|
||||
static DateTime? _parseScheduleDate(String rawValue) {
|
||||
final trimmed = rawValue.trim();
|
||||
if (trimmed.isEmpty) return null;
|
||||
|
||||
final parsedIso = DateTime.tryParse(trimmed);
|
||||
if (parsedIso != null) {
|
||||
return DateTime(parsedIso.year, parsedIso.month, parsedIso.day);
|
||||
}
|
||||
|
||||
final match = _flexDateKeyRegex.firstMatch(trimmed);
|
||||
if (match == null) return null;
|
||||
final year = int.tryParse(match.group(1) ?? '');
|
||||
final month = int.tryParse(match.group(2) ?? '');
|
||||
final day = int.tryParse(match.group(3) ?? '');
|
||||
if (year == null || month == null || day == null) return null;
|
||||
if (month < 1 || month > 12 || day < 1 || day > 31) return null;
|
||||
return DateTime(year, month, day);
|
||||
}
|
||||
|
||||
static ScheduleCacheStatus fromSchedules(
|
||||
Iterable<DailyPrayerSchedule> schedules,
|
||||
@@ -33,10 +53,10 @@ class ScheduleCacheStatus {
|
||||
) {
|
||||
DateTime? startDate;
|
||||
DateTime? endDate;
|
||||
var cachedDays = 0;
|
||||
final uniqueDayKeys = <String>{};
|
||||
|
||||
for (final schedule in schedules) {
|
||||
final parsedDate = DateTime.tryParse(schedule.date);
|
||||
final parsedDate = _parseScheduleDate(schedule.date);
|
||||
if (parsedDate == null) continue;
|
||||
|
||||
final normalized = DateTime(
|
||||
@@ -45,7 +65,9 @@ class ScheduleCacheStatus {
|
||||
parsedDate.day,
|
||||
);
|
||||
|
||||
cachedDays++;
|
||||
final dayKey =
|
||||
'${normalized.year}-${normalized.month.toString().padLeft(2, '0')}-${normalized.day.toString().padLeft(2, '0')}';
|
||||
uniqueDayKeys.add(dayKey);
|
||||
startDate = startDate == null || normalized.isBefore(startDate)
|
||||
? normalized
|
||||
: startDate;
|
||||
@@ -54,6 +76,7 @@ class ScheduleCacheStatus {
|
||||
: endDate;
|
||||
}
|
||||
|
||||
final cachedDays = uniqueDayKeys.length;
|
||||
if (startDate == null || endDate == null || cachedDays == 0) {
|
||||
return const ScheduleCacheStatus.empty();
|
||||
}
|
||||
@@ -78,6 +101,54 @@ class SyncService {
|
||||
SyncService._();
|
||||
static final SyncService instance = SyncService._();
|
||||
static const Duration _autoRefreshCooldown = Duration(hours: 12);
|
||||
static final DateFormat _dateKeyFormat = DateFormat('yyyy-MM-dd');
|
||||
static final RegExp _flexDateKeyRegex = RegExp(r'^(\d{4})-(\d{1,2})-(\d{1,2})$');
|
||||
|
||||
DateTime _dateOnly(DateTime value) => DateTime(value.year, value.month, value.day);
|
||||
|
||||
String _canonicalDateKey(DateTime value) => _dateKeyFormat.format(_dateOnly(value));
|
||||
|
||||
DateTime? _parseScheduleDate(String rawValue) {
|
||||
final trimmed = rawValue.trim();
|
||||
if (trimmed.isEmpty) return null;
|
||||
|
||||
final parsedIso = DateTime.tryParse(trimmed);
|
||||
if (parsedIso != null) return _dateOnly(parsedIso);
|
||||
|
||||
final match = _flexDateKeyRegex.firstMatch(trimmed);
|
||||
if (match == null) return null;
|
||||
final year = int.tryParse(match.group(1) ?? '');
|
||||
final month = int.tryParse(match.group(2) ?? '');
|
||||
final day = int.tryParse(match.group(3) ?? '');
|
||||
if (year == null || month == null || day == null) return null;
|
||||
if (month < 1 || month > 12 || day < 1 || day > 31) return null;
|
||||
return DateTime(year, month, day);
|
||||
}
|
||||
|
||||
String? _canonicalDateKeyFromRaw(String rawValue) {
|
||||
final parsed = _parseScheduleDate(rawValue);
|
||||
if (parsed == null) return null;
|
||||
return _canonicalDateKey(parsed);
|
||||
}
|
||||
|
||||
List<String> _lookupKeysForDate(DateTime value) {
|
||||
final date = _dateOnly(value);
|
||||
final canonical = _canonicalDateKey(date);
|
||||
final loose = '${date.year}-${date.month}-${date.day}';
|
||||
final yearMonthLooseDayPadded =
|
||||
'${date.year}-${date.month}-${date.day.toString().padLeft(2, '0')}';
|
||||
final yearMonthPaddedDayLoose =
|
||||
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day}';
|
||||
return <String>{
|
||||
canonical,
|
||||
loose,
|
||||
yearMonthLooseDayPadded,
|
||||
yearMonthPaddedDayLoose,
|
||||
}.toList();
|
||||
}
|
||||
|
||||
bool _isSameDate(DateTime left, DateTime right) =>
|
||||
left.year == right.year && left.month == right.month && left.day == right.day;
|
||||
|
||||
static Set<String> rollingWindowMonths(DateTime referenceDate) {
|
||||
final currentMonth = DateTime(referenceDate.year, referenceDate.month, 1);
|
||||
@@ -96,7 +167,7 @@ class SyncService {
|
||||
final staleKeys = <String>[];
|
||||
|
||||
for (final key in keys) {
|
||||
final parsed = DateTime.tryParse(key);
|
||||
final parsed = instance._parseScheduleDate(key);
|
||||
if (parsed == null) {
|
||||
staleKeys.add(key);
|
||||
continue;
|
||||
@@ -165,10 +236,11 @@ class SyncService {
|
||||
hasCurrentMonth = true;
|
||||
for (final entry in currentData.entries) {
|
||||
final jadwal = entry.value;
|
||||
final canonicalKey = _canonicalDateKeyFromRaw(entry.key) ?? entry.key;
|
||||
scheduleBox.put(
|
||||
entry.key,
|
||||
canonicalKey,
|
||||
DailyPrayerSchedule(
|
||||
date: entry.key,
|
||||
date: canonicalKey,
|
||||
imsak: jadwal['imsak'] ?? '00:00',
|
||||
subuh: jadwal['subuh'] ?? '00:00',
|
||||
terbit: jadwal['terbit'] ?? '00:00',
|
||||
@@ -179,6 +251,9 @@ class SyncService {
|
||||
isya: jadwal['isya'] ?? '00:00',
|
||||
),
|
||||
);
|
||||
if (canonicalKey != entry.key && scheduleBox.containsKey(entry.key)) {
|
||||
await scheduleBox.delete(entry.key);
|
||||
}
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
@@ -189,10 +264,11 @@ class SyncService {
|
||||
hasNextMonth = true;
|
||||
for (final entry in nextData.entries) {
|
||||
final jadwal = entry.value;
|
||||
final canonicalKey = _canonicalDateKeyFromRaw(entry.key) ?? entry.key;
|
||||
scheduleBox.put(
|
||||
entry.key,
|
||||
canonicalKey,
|
||||
DailyPrayerSchedule(
|
||||
date: entry.key,
|
||||
date: canonicalKey,
|
||||
imsak: jadwal['imsak'] ?? '00:00',
|
||||
subuh: jadwal['subuh'] ?? '00:00',
|
||||
terbit: jadwal['terbit'] ?? '00:00',
|
||||
@@ -203,6 +279,9 @@ class SyncService {
|
||||
isya: jadwal['isya'] ?? '00:00',
|
||||
),
|
||||
);
|
||||
if (canonicalKey != entry.key && scheduleBox.containsKey(entry.key)) {
|
||||
await scheduleBox.delete(entry.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,9 +332,20 @@ class SyncService {
|
||||
DailyPrayerSchedule? getTodaySchedule([DateTime? targetDate]) {
|
||||
final scheduleBox =
|
||||
Hive.box<DailyPrayerSchedule>(HiveBoxes.prayerSchedule);
|
||||
final dateToFetch = targetDate ?? DateTime.now();
|
||||
final dateStr = DateFormat('yyyy-MM-dd').format(dateToFetch);
|
||||
return scheduleBox.get(dateStr);
|
||||
final dateToFetch = _dateOnly(targetDate ?? DateTime.now());
|
||||
for (final key in _lookupKeysForDate(dateToFetch)) {
|
||||
final direct = scheduleBox.get(key);
|
||||
if (direct != null) return direct;
|
||||
}
|
||||
|
||||
// Legacy fallback: match by stored row date if cache keys were historical/mixed.
|
||||
for (final schedule in scheduleBox.values) {
|
||||
final parsed = _parseScheduleDate(schedule.date);
|
||||
if (parsed != null && _isSameDate(parsed, dateToFetch)) {
|
||||
return schedule;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
ScheduleCacheStatus getCacheStatus([DateTime? referenceDate]) {
|
||||
|
||||
Reference in New Issue
Block a user