Offline rediness
This commit is contained in:
+91
-113
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart' show debugPrint, kIsWeb;
|
||||
import 'package:flutter/foundation.dart' show ValueNotifier, debugPrint, kIsWeb;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
@@ -45,6 +45,9 @@ class CacheWarmer {
|
||||
static const _kTeamMembersKey = 'cache_team_members_json';
|
||||
static const _kUserOfficesKey = 'cache_user_offices_json';
|
||||
|
||||
/// Prefix for per-table row-count stamps written after each successful warm.
|
||||
static const _kCountPrefix = 'cache_count_';
|
||||
|
||||
/// Foreground timer handle so [start]/[stop] is idempotent.
|
||||
static Timer? _refreshTimer;
|
||||
|
||||
@@ -52,6 +55,9 @@ class CacheWarmer {
|
||||
static bool _hasWarmedOnce = false;
|
||||
static bool get hasWarmedOnce => _hasWarmedOnce;
|
||||
|
||||
/// Fires true when a warm starts, false when it completes.
|
||||
static final ValueNotifier<bool> warmingNotifier = ValueNotifier<bool>(false);
|
||||
|
||||
/// Single-flight guard to prevent overlapping warms.
|
||||
static Future<void>? _inflight;
|
||||
|
||||
@@ -62,9 +68,21 @@ class CacheWarmer {
|
||||
final existing = _inflight;
|
||||
if (existing != null) return existing;
|
||||
|
||||
warmingNotifier.value = true;
|
||||
final future = _runWarmAll(client);
|
||||
_inflight = future;
|
||||
return future.whenComplete(() => _inflight = null);
|
||||
// Guarantee the warming flag is cleared within 90 s even if an HTTP request
|
||||
// hangs (no per-request timeout on the Supabase client by default).
|
||||
return future
|
||||
.timeout(
|
||||
const Duration(seconds: 90),
|
||||
onTimeout: () =>
|
||||
debugPrint('[CacheWarmer] warmAll timeout — clearing warming flag'),
|
||||
)
|
||||
.whenComplete(() {
|
||||
_inflight = null;
|
||||
warmingNotifier.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
static Future<void> _runWarmAll(SupabaseClient client) async {
|
||||
@@ -121,24 +139,28 @@ class CacheWarmer {
|
||||
for (final row in rows) {
|
||||
await _localUpsert<Office>(Office.fromMap(row));
|
||||
}
|
||||
await _writeCount('offices', rows.length);
|
||||
}),
|
||||
_safe('services', () async {
|
||||
final rows = await client.from('services').select();
|
||||
for (final row in rows) {
|
||||
await _localUpsert<Service>(Service.fromMap(row));
|
||||
}
|
||||
await _writeCount('services', rows.length);
|
||||
}),
|
||||
_safe('profiles', () async {
|
||||
final rows = await client.from('profiles').select();
|
||||
for (final row in rows) {
|
||||
await _localUpsert<Profile>(Profile.fromMap(row));
|
||||
}
|
||||
await _writeCount('profiles', rows.length);
|
||||
}),
|
||||
_safe('teams', () async {
|
||||
final rows = await client.from('teams').select();
|
||||
for (final row in rows) {
|
||||
await _localUpsert<Team>(Team.fromMap(row));
|
||||
}
|
||||
await _writeCount('teams', rows.length);
|
||||
}),
|
||||
// Association tables have composite PKs, so we cache them as JSON in
|
||||
// SharedPreferences instead of Brick. The list is small and read-only.
|
||||
@@ -146,6 +168,7 @@ class CacheWarmer {
|
||||
final rows = await client.from('team_members').select();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_kTeamMembersKey, jsonEncode(rows));
|
||||
await _writeCount('team_members', rows.length);
|
||||
}),
|
||||
_safe('user_offices', () async {
|
||||
final rows = await client.from('user_offices').select();
|
||||
@@ -189,147 +212,77 @@ class CacheWarmer {
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
static Future<void> _warmTasks(SupabaseClient client, String sinceIso) async {
|
||||
final rows = await client
|
||||
.from('tasks')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<Task>(Task.fromMap(row));
|
||||
}
|
||||
final rows = await client.from('tasks').select().gte('created_at', sinceIso);
|
||||
for (final row in rows) { await _localUpsert<Task>(Task.fromMap(row)); }
|
||||
await _writeCount('tasks', rows.length);
|
||||
}
|
||||
|
||||
static Future<void> _warmTickets(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
final rows = await client
|
||||
.from('tickets')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<Ticket>(Ticket.fromMap(row));
|
||||
}
|
||||
static Future<void> _warmTickets(SupabaseClient client, String sinceIso) async {
|
||||
final rows = await client.from('tickets').select().gte('created_at', sinceIso);
|
||||
for (final row in rows) { await _localUpsert<Ticket>(Ticket.fromMap(row)); }
|
||||
await _writeCount('tickets', rows.length);
|
||||
}
|
||||
|
||||
static Future<void> _warmItServiceRequests(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
final rows = await client
|
||||
.from('it_service_requests')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<ItServiceRequest>(ItServiceRequest.fromMap(row));
|
||||
}
|
||||
static Future<void> _warmItServiceRequests(SupabaseClient client, String sinceIso) async {
|
||||
final rows = await client.from('it_service_requests').select().gte('created_at', sinceIso);
|
||||
for (final row in rows) { await _localUpsert<ItServiceRequest>(ItServiceRequest.fromMap(row)); }
|
||||
await _writeCount('it_service_requests', rows.length);
|
||||
}
|
||||
|
||||
static Future<void> _warmAnnouncements(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
final rows = await client
|
||||
.from('announcements')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<Announcement>(Announcement.fromMap(row));
|
||||
}
|
||||
// Also pull comments for the windowed announcements so detail screens have
|
||||
// them offline.
|
||||
static Future<void> _warmAnnouncements(SupabaseClient client, String sinceIso) async {
|
||||
final rows = await client.from('announcements').select().gte('created_at', sinceIso);
|
||||
for (final row in rows) { await _localUpsert<Announcement>(Announcement.fromMap(row)); }
|
||||
await _writeCount('announcements', rows.length);
|
||||
// Also pull comments so detail screens have them offline.
|
||||
try {
|
||||
final commentRows = await client
|
||||
.from('announcement_comments')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in commentRows) {
|
||||
await _localUpsert<AnnouncementComment>(
|
||||
AnnouncementComment.fromMap(row),
|
||||
);
|
||||
await _localUpsert<AnnouncementComment>(AnnouncementComment.fromMap(row));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[CacheWarmer] announcement_comments failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _warmAttendance(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
final rows = await client
|
||||
.from('attendance_logs')
|
||||
.select()
|
||||
.gte('check_in_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<AttendanceLog>(AttendanceLog.fromMap(row));
|
||||
}
|
||||
static Future<void> _warmAttendance(SupabaseClient client, String sinceIso) async {
|
||||
final rows = await client.from('attendance_logs').select().gte('check_in_at', sinceIso);
|
||||
for (final row in rows) { await _localUpsert<AttendanceLog>(AttendanceLog.fromMap(row)); }
|
||||
await _writeCount('attendance_logs', rows.length);
|
||||
}
|
||||
|
||||
static Future<void> _warmPassSlips(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
final rows = await client
|
||||
.from('pass_slips')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<PassSlip>(PassSlip.fromMap(row));
|
||||
}
|
||||
static Future<void> _warmPassSlips(SupabaseClient client, String sinceIso) async {
|
||||
final rows = await client.from('pass_slips').select().gte('created_at', sinceIso);
|
||||
for (final row in rows) { await _localUpsert<PassSlip>(PassSlip.fromMap(row)); }
|
||||
await _writeCount('pass_slips', rows.length);
|
||||
}
|
||||
|
||||
static Future<void> _warmLeaves(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
final rows = await client
|
||||
.from('leave_of_absence')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<LeaveOfAbsence>(LeaveOfAbsence.fromMap(row));
|
||||
}
|
||||
static Future<void> _warmLeaves(SupabaseClient client, String sinceIso) async {
|
||||
final rows = await client.from('leave_of_absence').select().gte('created_at', sinceIso);
|
||||
for (final row in rows) { await _localUpsert<LeaveOfAbsence>(LeaveOfAbsence.fromMap(row)); }
|
||||
await _writeCount('leave_of_absence', rows.length);
|
||||
}
|
||||
|
||||
static Future<void> _warmDutySchedules(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
static Future<void> _warmDutySchedules(SupabaseClient client, String sinceIso) async {
|
||||
// Duty schedules are upcoming-focused; pull a wider window so the user
|
||||
// can see today + the next several weeks while offline.
|
||||
final rows = await client
|
||||
.from('duty_schedules')
|
||||
.select()
|
||||
.gte('start_time', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<DutySchedule>(DutySchedule.fromMap(row));
|
||||
}
|
||||
final rows = await client.from('duty_schedules').select().gte('start_time', sinceIso);
|
||||
for (final row in rows) { await _localUpsert<DutySchedule>(DutySchedule.fromMap(row)); }
|
||||
await _writeCount('duty_schedules', rows.length);
|
||||
}
|
||||
|
||||
static Future<void> _warmSwapRequests(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
final rows = await client
|
||||
.from('swap_requests')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<SwapRequest>(SwapRequest.fromMap(row));
|
||||
}
|
||||
static Future<void> _warmSwapRequests(SupabaseClient client, String sinceIso) async {
|
||||
final rows = await client.from('swap_requests').select().gte('created_at', sinceIso);
|
||||
for (final row in rows) { await _localUpsert<SwapRequest>(SwapRequest.fromMap(row)); }
|
||||
await _writeCount('swap_requests', rows.length);
|
||||
}
|
||||
|
||||
static Future<void> _warmNotifications(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
final rows = await client
|
||||
.from('notifications')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<NotificationItem>(NotificationItem.fromMap(row));
|
||||
}
|
||||
static Future<void> _warmNotifications(SupabaseClient client, String sinceIso) async {
|
||||
final rows = await client.from('notifications').select().gte('created_at', sinceIso);
|
||||
for (final row in rows) { await _localUpsert<NotificationItem>(NotificationItem.fromMap(row)); }
|
||||
await _writeCount('notifications', rows.length);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
@@ -357,4 +310,29 @@ class CacheWarmer {
|
||||
}());
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist the number of rows cached for [table] to SharedPreferences.
|
||||
/// Called at the end of each per-table warm so the Offline Readiness screen
|
||||
/// can display cache coverage without issuing SQLite count queries.
|
||||
static Future<void> _writeCount(String table, int count) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt('$_kCountPrefix$table', count);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/// Returns cached row counts written by the last successful warm, keyed by
|
||||
/// table name. A value of -1 means the table has never been warmed.
|
||||
static Future<Map<String, int>> readCacheCounts() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
const tables = [
|
||||
'tasks', 'tickets', 'announcements', 'attendance_logs',
|
||||
'duty_schedules', 'pass_slips', 'leave_of_absence', 'profiles',
|
||||
'teams', 'team_members', 'it_service_requests', 'notifications',
|
||||
'swap_requests', 'offices', 'services',
|
||||
];
|
||||
return {
|
||||
for (final t in tables) t: prefs.getInt('$_kCountPrefix$t') ?? -1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user