Files
iiEsaywebUIapp/lib/core/services/persistent_streaming_service.dart

485 lines
14 KiB
Dart
Raw Normal View History

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:dio/dio.dart';
import 'background_streaming_handler.dart';
import 'connectivity_service.dart';
2025-08-20 22:15:26 +05:30
import '../utils/debug_logger.dart';
class PersistentStreamingService with WidgetsBindingObserver {
2025-08-20 22:15:26 +05:30
static final PersistentStreamingService _instance =
PersistentStreamingService._internal();
factory PersistentStreamingService() => _instance;
PersistentStreamingService._internal() {
_initialize();
}
// Active streams registry
final Map<String, StreamSubscription> _activeStreams = {};
final Map<String, StreamController> _streamControllers = {};
final Map<String, Function> _streamRecoveryCallbacks = {};
final Map<String, Map<String, dynamic>> _streamMetadata = {};
2025-08-20 22:15:26 +05:30
// App lifecycle state
// AppLifecycleState? _lastLifecycleState; // Removed as it's unused
bool _isInBackground = false;
Timer? _backgroundTimer;
Timer? _heartbeatTimer;
2025-08-20 22:15:26 +05:30
// Background streaming handler
late final BackgroundStreamingHandler _backgroundHandler;
2025-08-20 22:15:26 +05:30
// Connectivity monitoring
StreamSubscription<bool>? _connectivitySubscription;
bool _hasConnectivity = true;
2025-08-20 22:15:26 +05:30
// Recovery state
final Map<String, int> _retryAttempts = {};
static const int _maxRetryAttempts = 3;
static const Duration _retryDelay = Duration(seconds: 2);
2025-08-20 22:15:26 +05:30
void _initialize() {
WidgetsBinding.instance.addObserver(this);
_backgroundHandler = BackgroundStreamingHandler.instance;
_setupBackgroundHandlerCallbacks();
_setupConnectivityMonitoring();
_startHeartbeat();
}
2025-08-20 22:15:26 +05:30
void _setupBackgroundHandlerCallbacks() {
_backgroundHandler.onStreamsSuspending = (streamIds) {
2025-08-20 22:15:26 +05:30
DebugLogger.stream(
'PersistentStreaming: Streams suspending - $streamIds',
);
// Mark streams as suspended but don't close them yet
for (final streamId in streamIds) {
_markStreamAsSuspended(streamId);
}
};
2025-08-20 22:15:26 +05:30
_backgroundHandler.onBackgroundTaskExpiring = () {
2025-08-20 22:15:26 +05:30
DebugLogger.stream('PersistentStreaming: Background task expiring');
// Save states and prepare for recovery
_saveStreamStatesForRecovery();
};
2025-08-20 22:15:26 +05:30
_backgroundHandler.shouldContinueInBackground = () {
return _activeStreams.isNotEmpty;
};
}
2025-08-20 22:15:26 +05:30
void _setupConnectivityMonitoring() {
// Create a connectivity service instance - this would normally be injected
// For now, create a temporary instance just for monitoring
final connectivityService = ConnectivityService(Dio());
2025-08-20 22:15:26 +05:30
_connectivitySubscription = connectivityService.isConnected.listen((
connected,
) {
final wasConnected = _hasConnectivity;
_hasConnectivity = connected;
2025-08-20 22:15:26 +05:30
if (!wasConnected && connected) {
// Connectivity restored - try to recover streams
2025-08-20 22:15:26 +05:30
DebugLogger.stream(
'PersistentStreaming: Connectivity restored, recovering streams',
);
_recoverActiveStreams();
} else if (wasConnected && !connected) {
// Connectivity lost - mark streams as suspended
2025-08-20 22:15:26 +05:30
DebugLogger.stream(
'PersistentStreaming: Connectivity lost, suspending streams',
);
_suspendAllStreams();
}
});
}
2025-08-20 22:15:26 +05:30
void _startHeartbeat() {
_heartbeatTimer = Timer.periodic(const Duration(seconds: 30), (_) {
if (_activeStreams.isNotEmpty && _isInBackground) {
_backgroundHandler.keepAlive();
}
});
}
2025-08-20 22:15:26 +05:30
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// _lastLifecycleState = state; // Removed as it's unused
2025-08-20 22:15:26 +05:30
switch (state) {
case AppLifecycleState.paused:
case AppLifecycleState.inactive:
_onAppBackground();
break;
case AppLifecycleState.resumed:
_onAppForeground();
break;
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
// Handle app termination
_onAppDetached();
break;
}
}
2025-08-20 22:15:26 +05:30
void _onAppBackground() {
2025-08-20 22:15:26 +05:30
DebugLogger.stream('PersistentStreamingService: App went to background');
_isInBackground = true;
2025-08-20 22:15:26 +05:30
// Enable wake lock to prevent device sleep during streaming
if (_activeStreams.isNotEmpty) {
_enableWakeLock();
_startBackgroundExecution();
}
}
2025-08-20 22:15:26 +05:30
void _onAppForeground() {
2025-08-20 22:15:26 +05:30
DebugLogger.stream(
'PersistentStreamingService: App returned to foreground',
);
_isInBackground = false;
2025-08-20 22:15:26 +05:30
// Cancel background timer
_backgroundTimer?.cancel();
_backgroundTimer = null;
2025-08-20 22:15:26 +05:30
// Disable wake lock if no active streams
if (_activeStreams.isEmpty) {
_disableWakeLock();
}
2025-08-20 22:15:26 +05:30
// Check and recover any interrupted streams
_recoverActiveStreams();
}
2025-08-20 22:15:26 +05:30
void _onAppDetached() {
2025-08-20 22:15:26 +05:30
DebugLogger.stream('PersistentStreamingService: App detached');
// Save stream states for recovery
_saveStreamStatesForRecovery();
2025-08-20 22:15:26 +05:30
// Clean up
_backgroundTimer?.cancel();
_heartbeatTimer?.cancel();
_disableWakeLock();
}
2025-08-20 22:15:26 +05:30
// Register a stream for persistent handling
String registerStream({
required StreamSubscription subscription,
required StreamController controller,
Function? recoveryCallback,
Map<String, dynamic>? metadata,
}) {
final streamId = DateTime.now().millisecondsSinceEpoch.toString();
2025-08-20 22:15:26 +05:30
_activeStreams[streamId] = subscription;
_streamControllers[streamId] = controller;
if (recoveryCallback != null) {
_streamRecoveryCallbacks[streamId] = recoveryCallback;
}
2025-08-20 22:15:26 +05:30
// Store metadata for recovery
if (metadata != null) {
_streamMetadata[streamId] = metadata;
2025-08-20 22:15:26 +05:30
// Register with background handler
_backgroundHandler.registerStream(
streamId,
conversationId: metadata['conversationId'] ?? '',
messageId: metadata['messageId'] ?? '',
sessionId: metadata['sessionId'],
lastChunkSequence: metadata['lastChunkSequence'],
lastContent: metadata['lastContent'],
);
}
2025-08-20 22:15:26 +05:30
// Enable wake lock when streaming starts
if (_activeStreams.length == 1) {
_enableWakeLock();
}
2025-08-20 22:15:26 +05:30
// Start background execution if app is backgrounded
if (_isInBackground) {
_startBackgroundExecution();
}
2025-08-20 22:15:26 +05:30
DebugLogger.stream(
'PersistentStreamingService: Registered stream $streamId',
);
return streamId;
}
2025-08-20 22:15:26 +05:30
// Unregister a stream
void unregisterStream(String streamId) {
_activeStreams.remove(streamId);
_streamControllers.remove(streamId);
_streamRecoveryCallbacks.remove(streamId);
_streamMetadata.remove(streamId);
_retryAttempts.remove(streamId);
2025-08-20 22:15:26 +05:30
// Unregister from background handler
_backgroundHandler.unregisterStream(streamId);
2025-08-20 22:15:26 +05:30
// Stop background execution if no more streams
if (_activeStreams.isEmpty) {
_backgroundHandler.stopBackgroundExecution([streamId]);
_disableWakeLock();
}
2025-08-20 22:15:26 +05:30
DebugLogger.stream(
'PersistentStreamingService: Unregistered stream $streamId',
);
}
2025-08-20 22:15:26 +05:30
// Check if a stream is still active
bool isStreamActive(String streamId) {
return _activeStreams.containsKey(streamId);
}
2025-08-20 22:15:26 +05:30
// Recover interrupted streams
Future<void> _recoverActiveStreams() async {
if (!_hasConnectivity) {
2025-08-20 22:15:26 +05:30
DebugLogger.stream(
'PersistentStreaming: No connectivity, skipping recovery',
);
return;
}
2025-08-20 22:15:26 +05:30
// First, try to recover from background handler saved states
final savedStates = await _backgroundHandler.recoverStreamStates();
for (final state in savedStates) {
if (!state.isStale()) {
await _recoverStreamFromState(state);
}
}
2025-08-20 22:15:26 +05:30
// Then check active streams for recovery
for (final entry in _streamRecoveryCallbacks.entries) {
final streamId = entry.key;
final recoveryCallback = entry.value;
2025-08-20 22:15:26 +05:30
// Check if stream was interrupted or needs recovery
final subscription = _activeStreams[streamId];
if (subscription == null || _needsRecovery(streamId)) {
await _attemptStreamRecovery(streamId, recoveryCallback);
}
}
}
2025-08-20 22:15:26 +05:30
Future<void> _recoverStreamFromState(StreamState state) async {
final recoveryCallback = _streamRecoveryCallbacks[state.streamId];
if (recoveryCallback != null) {
2025-08-20 22:15:26 +05:30
DebugLogger.stream(
'PersistentStreaming: Recovering stream from saved state: ${state.streamId}',
);
await _attemptStreamRecovery(state.streamId, recoveryCallback);
}
}
2025-08-20 22:15:26 +05:30
Future<void> _attemptStreamRecovery(
String streamId,
Function recoveryCallback,
) async {
final attempts = _retryAttempts[streamId] ?? 0;
if (attempts >= _maxRetryAttempts) {
2025-08-20 22:15:26 +05:30
DebugLogger.warning(
'PersistentStreaming: Max retry attempts reached for stream $streamId',
);
return;
}
2025-08-20 22:15:26 +05:30
DebugLogger.stream(
'PersistentStreaming: Recovering stream $streamId (attempt ${attempts + 1})',
);
try {
_retryAttempts[streamId] = attempts + 1;
2025-08-20 22:15:26 +05:30
// Add exponential backoff delay
if (attempts > 0) {
final delay = _retryDelay * (1 << (attempts - 1)); // 2s, 4s, 8s...
await Future.delayed(delay);
}
2025-08-20 22:15:26 +05:30
// Call recovery callback to restart the stream
await recoveryCallback();
2025-08-20 22:15:26 +05:30
// Reset retry count on success
_retryAttempts.remove(streamId);
} catch (e) {
2025-08-20 22:15:26 +05:30
DebugLogger.error(
'PersistentStreaming: Failed to recover stream $streamId',
e,
);
// Schedule next retry if under limit
if (_retryAttempts[streamId]! < _maxRetryAttempts) {
2025-08-20 22:15:26 +05:30
Timer(
_retryDelay,
() => _attemptStreamRecovery(streamId, recoveryCallback),
);
}
}
}
2025-08-20 22:15:26 +05:30
bool _needsRecovery(String streamId) {
final metadata = _streamMetadata[streamId];
if (metadata == null) return false;
2025-08-20 22:15:26 +05:30
// Check if stream has been inactive for too long
final lastUpdate = metadata['lastUpdate'] as DateTime?;
if (lastUpdate != null) {
final timeSinceUpdate = DateTime.now().difference(lastUpdate);
return timeSinceUpdate > const Duration(minutes: 1);
}
2025-08-20 22:15:26 +05:30
return false;
}
2025-08-20 22:15:26 +05:30
// Platform-specific background execution
void _startBackgroundExecution() {
if (_activeStreams.isNotEmpty) {
_backgroundHandler.startBackgroundExecution(_activeStreams.keys.toList());
}
}
2025-08-20 22:15:26 +05:30
void _markStreamAsSuspended(String streamId) {
final metadata = _streamMetadata[streamId];
if (metadata != null) {
metadata['suspended'] = true;
metadata['suspendedAt'] = DateTime.now();
}
}
2025-08-20 22:15:26 +05:30
void _suspendAllStreams() {
for (final streamId in _activeStreams.keys) {
_markStreamAsSuspended(streamId);
}
}
2025-08-20 22:15:26 +05:30
void _saveStreamStatesForRecovery() {
// The background handler will handle the actual saving
2025-08-20 22:15:26 +05:30
DebugLogger.stream(
'PersistentStreaming: Saving ${_activeStreams.length} stream states for recovery',
);
}
2025-08-20 22:15:26 +05:30
// Update stream metadata when chunks are received
2025-08-20 22:15:26 +05:30
void updateStreamProgress(
String streamId, {
int? chunkSequence,
String? content,
String? appendedContent,
}) {
// Update background handler state
_backgroundHandler.updateStreamState(
streamId,
chunkSequence: chunkSequence,
content: content,
appendedContent: appendedContent,
);
2025-08-20 22:15:26 +05:30
// Update local metadata
final metadata = _streamMetadata[streamId];
if (metadata != null) {
metadata['lastUpdate'] = DateTime.now();
2025-08-20 22:15:26 +05:30
metadata['lastChunkSequence'] =
chunkSequence ?? metadata['lastChunkSequence'];
if (appendedContent != null) {
2025-08-20 22:15:26 +05:30
metadata['lastContent'] =
(metadata['lastContent'] ?? '') + appendedContent;
} else if (content != null) {
metadata['lastContent'] = content;
}
metadata['suspended'] = false; // Mark as active
}
}
2025-08-20 22:15:26 +05:30
// Wake lock management
void _enableWakeLock() async {
try {
await WakelockPlus.enable();
2025-08-20 22:15:26 +05:30
DebugLogger.stream('PersistentStreamingService: Wake lock enabled');
} catch (e) {
2025-08-20 22:15:26 +05:30
DebugLogger.error(
'PersistentStreamingService: Failed to enable wake lock',
e,
);
}
}
2025-08-20 22:15:26 +05:30
void _disableWakeLock() async {
try {
await WakelockPlus.disable();
2025-08-20 22:15:26 +05:30
DebugLogger.stream('PersistentStreamingService: Wake lock disabled');
} catch (e) {
2025-08-20 22:15:26 +05:30
DebugLogger.error(
'PersistentStreamingService: Failed to disable wake lock',
e,
);
}
}
2025-08-20 22:15:26 +05:30
// Get active stream count
int get activeStreamCount => _activeStreams.length;
2025-08-20 22:15:26 +05:30
// Get stream metadata
Map<String, dynamic>? getStreamMetadata(String streamId) {
return _streamMetadata[streamId];
}
2025-08-20 22:15:26 +05:30
// Check if stream is suspended
bool isStreamSuspended(String streamId) {
final metadata = _streamMetadata[streamId];
return metadata?['suspended'] == true;
}
2025-08-20 22:15:26 +05:30
// Force recovery of a specific stream
Future<void> forceRecoverStream(String streamId) async {
final recoveryCallback = _streamRecoveryCallbacks[streamId];
if (recoveryCallback != null) {
_retryAttempts.remove(streamId); // Reset retry count
await _attemptStreamRecovery(streamId, recoveryCallback);
}
}
2025-08-20 22:15:26 +05:30
// Cleanup
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_backgroundTimer?.cancel();
_heartbeatTimer?.cancel();
_connectivitySubscription?.cancel();
_disableWakeLock();
2025-08-20 22:15:26 +05:30
// Stop all background execution
if (_activeStreams.isNotEmpty) {
_backgroundHandler.stopBackgroundExecution(_activeStreams.keys.toList());
}
2025-08-20 22:15:26 +05:30
// Cancel all active streams
for (final subscription in _activeStreams.values) {
subscription.cancel();
}
_activeStreams.clear();
2025-08-20 22:15:26 +05:30
// Close all controllers
for (final controller in _streamControllers.values) {
if (!controller.isClosed) {
controller.close();
}
}
_streamControllers.clear();
2025-08-20 22:15:26 +05:30
// Clear all metadata
_streamMetadata.clear();
_streamRecoveryCallbacks.clear();
_retryAttempts.clear();
2025-08-20 22:15:26 +05:30
// Clear background handler
_backgroundHandler.clearAll();
}
2025-08-20 22:15:26 +05:30
}