feat: background loading of chats
This commit is contained in:
@@ -10,8 +10,78 @@ import '../models/conversation.dart';
|
|||||||
import '../services/background_streaming_handler.dart';
|
import '../services/background_streaming_handler.dart';
|
||||||
import '../../features/onboarding/views/onboarding_sheet.dart';
|
import '../../features/onboarding/views/onboarding_sheet.dart';
|
||||||
import '../../shared/theme/theme_extensions.dart';
|
import '../../shared/theme/theme_extensions.dart';
|
||||||
|
import '../services/connectivity_service.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
|
|
||||||
|
enum _ConversationWarmupStatus { idle, warming, complete }
|
||||||
|
|
||||||
|
final _conversationWarmupStatusProvider =
|
||||||
|
StateProvider<_ConversationWarmupStatus>(
|
||||||
|
(ref) => _ConversationWarmupStatus.idle,
|
||||||
|
);
|
||||||
|
|
||||||
|
final _conversationWarmupLastAttemptProvider = StateProvider<DateTime?>(
|
||||||
|
(ref) => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
void _scheduleConversationWarmup(Ref ref, {bool force = false}) {
|
||||||
|
final navState = ref.read(authNavigationStateProvider);
|
||||||
|
if (navState != AuthNavigationState.authenticated) {
|
||||||
|
ref.read(_conversationWarmupStatusProvider.notifier).state =
|
||||||
|
_ConversationWarmupStatus.idle;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final isOnline = ref.read(isOnlineProvider);
|
||||||
|
if (!isOnline) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final statusController = ref.read(_conversationWarmupStatusProvider.notifier);
|
||||||
|
final status = statusController.state;
|
||||||
|
|
||||||
|
if (!force) {
|
||||||
|
if (status == _ConversationWarmupStatus.warming ||
|
||||||
|
status == _ConversationWarmupStatus.complete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (status == _ConversationWarmupStatus.warming) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
final lastAttempt = ref.read(_conversationWarmupLastAttemptProvider);
|
||||||
|
if (!force &&
|
||||||
|
lastAttempt != null &&
|
||||||
|
now.difference(lastAttempt) < const Duration(seconds: 30)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ref.read(_conversationWarmupLastAttemptProvider.notifier).state = now;
|
||||||
|
|
||||||
|
statusController.state = _ConversationWarmupStatus.warming;
|
||||||
|
|
||||||
|
Future.microtask(() async {
|
||||||
|
try {
|
||||||
|
final existing = ref.read(conversationsProvider);
|
||||||
|
if (existing.hasValue) {
|
||||||
|
statusController.state = _ConversationWarmupStatus.complete;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (existing.hasError) {
|
||||||
|
ref.invalidate(conversationsProvider);
|
||||||
|
}
|
||||||
|
final conversations = await ref.read(conversationsProvider.future);
|
||||||
|
statusController.state = _ConversationWarmupStatus.complete;
|
||||||
|
DebugLogger.info(
|
||||||
|
'Background chats warmup fetched ${conversations.length} conversations',
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
DebugLogger.warning('Background chats warmup failed: $error');
|
||||||
|
statusController.state = _ConversationWarmupStatus.idle;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// App-level startup/background task flow orchestrator.
|
/// App-level startup/background task flow orchestrator.
|
||||||
///
|
///
|
||||||
/// Moves background initialization out of widgets and into a Riverpod provider,
|
/// Moves background initialization out of widgets and into a Riverpod provider,
|
||||||
@@ -36,18 +106,18 @@ final appStartupFlowProvider = Provider<void>((ref) {
|
|||||||
// Keep Socket.IO connection alive in background within platform limits
|
// Keep Socket.IO connection alive in background within platform limits
|
||||||
ref.watch(socketPersistenceProvider);
|
ref.watch(socketPersistenceProvider);
|
||||||
|
|
||||||
// When auth state becomes authenticated, run additional background work
|
// Warm the conversations list in the background as soon as possible
|
||||||
ref.listen<AuthNavigationState>(authNavigationStateProvider,
|
Future.microtask(() => _scheduleConversationWarmup(ref));
|
||||||
(prev, next) {
|
|
||||||
|
// Watch for auth transitions to trigger warmup and other background work
|
||||||
|
ref.listen<AuthNavigationState>(authNavigationStateProvider, (prev, next) {
|
||||||
if (next == AuthNavigationState.authenticated) {
|
if (next == AuthNavigationState.authenticated) {
|
||||||
// Schedule microtask so we don't perform side-effects inside build
|
// Schedule microtask so we don't perform side-effects inside build
|
||||||
Future.microtask(() async {
|
Future.microtask(() async {
|
||||||
try {
|
try {
|
||||||
final api = ref.read(apiServiceProvider);
|
final api = ref.read(apiServiceProvider);
|
||||||
if (api == null) {
|
if (api == null) {
|
||||||
DebugLogger.warning(
|
DebugLogger.warning('API service not available for startup flow');
|
||||||
'API service not available for startup flow',
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,15 +132,44 @@ final appStartupFlowProvider = Provider<void>((ref) {
|
|||||||
try {
|
try {
|
||||||
await ref.read(defaultModelProvider.future);
|
await ref.read(defaultModelProvider.future);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DebugLogger.warning('StartupFlow: default model preload failed: $e');
|
DebugLogger.warning(
|
||||||
|
'StartupFlow: default model preload failed: $e',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kick background chat warmup now that we're authenticated
|
||||||
|
_scheduleConversationWarmup(ref, force: true);
|
||||||
|
|
||||||
// Show onboarding once when user reaches chat and hasn't seen it yet
|
// Show onboarding once when user reaches chat and hasn't seen it yet
|
||||||
await _maybeShowOnboarding(ref);
|
await _maybeShowOnboarding(ref);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DebugLogger.error('StartupFlow error', e);
|
DebugLogger.error('StartupFlow error', e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Reset warmup state when leaving authenticated flow
|
||||||
|
ref.read(_conversationWarmupStatusProvider.notifier).state =
|
||||||
|
_ConversationWarmupStatus.idle;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retry warmup when connectivity is restored
|
||||||
|
ref.listen<bool>(isOnlineProvider, (prev, next) {
|
||||||
|
if (next == true) {
|
||||||
|
_scheduleConversationWarmup(ref);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// When conversations reload (e.g., manual refresh), ensure warmup runs again
|
||||||
|
ref.listen<AsyncValue<List<Conversation>>>(conversationsProvider, (
|
||||||
|
previous,
|
||||||
|
next,
|
||||||
|
) {
|
||||||
|
final wasReady = previous?.hasValue == true || previous?.hasError == true;
|
||||||
|
if (wasReady && next.isLoading) {
|
||||||
|
ref.read(_conversationWarmupStatusProvider.notifier).state =
|
||||||
|
_ConversationWarmupStatus.idle;
|
||||||
|
Future.microtask(() => _scheduleConversationWarmup(ref, force: true));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -96,7 +195,10 @@ class _ForegroundRefreshObserver extends WidgetsBindingObserver {
|
|||||||
Future.microtask(() {
|
Future.microtask(() {
|
||||||
try {
|
try {
|
||||||
_ref.invalidate(conversationsProvider);
|
_ref.invalidate(conversationsProvider);
|
||||||
|
_ref.read(_conversationWarmupStatusProvider.notifier).state =
|
||||||
|
_ConversationWarmupStatus.idle;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
_scheduleConversationWarmup(_ref, force: true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,7 +234,7 @@ class _SocketPersistenceObserver extends WidgetsBindingObserver {
|
|||||||
bool _shouldKeepAlive() {
|
bool _shouldKeepAlive() {
|
||||||
final authed =
|
final authed =
|
||||||
_ref.read(authNavigationStateProvider) ==
|
_ref.read(authNavigationStateProvider) ==
|
||||||
AuthNavigationState.authenticated;
|
AuthNavigationState.authenticated;
|
||||||
final hasConversation = _ref.read(activeConversationProvider) != null;
|
final hasConversation = _ref.read(activeConversationProvider) != null;
|
||||||
return authed && hasConversation;
|
return authed && hasConversation;
|
||||||
}
|
}
|
||||||
@@ -141,12 +243,10 @@ class _SocketPersistenceObserver extends WidgetsBindingObserver {
|
|||||||
if (_bgActive) return;
|
if (_bgActive) return;
|
||||||
if (!_shouldKeepAlive()) return;
|
if (!_shouldKeepAlive()) return;
|
||||||
try {
|
try {
|
||||||
BackgroundStreamingHandler.instance
|
BackgroundStreamingHandler.instance.startBackgroundExecution([_socketId]);
|
||||||
.startBackgroundExecution([_socketId]);
|
|
||||||
// Periodic keep-alive (primarily useful on iOS)
|
// Periodic keep-alive (primarily useful on iOS)
|
||||||
_heartbeat?.cancel();
|
_heartbeat?.cancel();
|
||||||
_heartbeat =
|
_heartbeat = Timer.periodic(const Duration(seconds: 30), (_) async {
|
||||||
Timer.periodic(const Duration(seconds: 30), (_) async {
|
|
||||||
try {
|
try {
|
||||||
await BackgroundStreamingHandler.instance.keepAlive();
|
await BackgroundStreamingHandler.instance.keepAlive();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -158,8 +258,7 @@ class _SocketPersistenceObserver extends WidgetsBindingObserver {
|
|||||||
void _stopBackground() {
|
void _stopBackground() {
|
||||||
if (!_bgActive) return;
|
if (!_bgActive) return;
|
||||||
try {
|
try {
|
||||||
BackgroundStreamingHandler.instance
|
BackgroundStreamingHandler.instance.stopBackgroundExecution([_socketId]);
|
||||||
.stopBackgroundExecution([_socketId]);
|
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
_heartbeat?.cancel();
|
_heartbeat?.cancel();
|
||||||
_heartbeat = null;
|
_heartbeat = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user