library; import 'dart:async'; import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:smart_attendance_app/core/constants.dart'; import 'package:smart_attendance_app/data/local/secure_storage.dart'; import 'package:smart_attendance_app/domain/enums/auth_state.dart'; import 'package:smart_attendance_app/features/auth/providers/auth_provider.dart'; import 'package:smart_attendance_app/utils/logger.dart'; final websocketServiceProvider = Provider((ref) { final service = WebSocketService( secureStorage: ref.read(secureStorageProvider), ); final authState = ref.read(authProvider); if (authState.status == AuthStatus.authenticated) { service.connect(); } ref.listen(authProvider, (previous, next) { if (next.status == AuthStatus.authenticated) { service.connect(); } else if (next.status == AuthStatus.unauthenticated) { service.disconnect(); } }); ref.onDispose(() { service.dispose(); }); return service; }); enum WebSocketStatus { disconnected, connecting, connected, error } class WebSocketService { final SecureStorageService _secureStorage; WebSocketChannel? _channel; StreamSubscription? _subscription; WebSocketStatus _status = WebSocketStatus.disconnected; final _messageController = StreamController>.broadcast(); Timer? _reconnectTimer; Timer? _pingTimer; int _reconnectAttempts = 0; static const int _maxReconnectAttempts = 5; static const Duration _reconnectDelay = Duration(seconds: 3); static const Duration _pingInterval = Duration(seconds: 30); WebSocketService({required SecureStorageService secureStorage}) : _secureStorage = secureStorage; WebSocketStatus get status => _status; Stream> get messageStream => _messageController.stream; Future connect() async { if (_status == WebSocketStatus.connected || _status == WebSocketStatus.connecting) { return; } _status = WebSocketStatus.connecting; final token = await _secureStorage.getToken(); if (token == null) { _status = WebSocketStatus.error; return; } try { final wsUrl = kApiBaseUrl.replaceFirst('http://', 'ws://').replaceFirst('https://', 'wss://'); final uri = Uri.parse('$wsUrl/ws/connect'); _channel = WebSocketChannel.connect(uri); _subscription = _channel!.stream.listen( _onMessage, onError: _onError, onDone: _onDone, cancelOnError: false, ); await _channel!.ready; _channel!.sink.add(jsonEncode({'type': 'auth', 'token': token})); _status = WebSocketStatus.connected; _reconnectAttempts = 0; _startPingTimer(); } catch (e) { AppLogger.error('WebSocket: Connection failed: $e'); _status = WebSocketStatus.error; _scheduleReconnect(); } } void _onMessage(dynamic message) { try { final data = jsonDecode(message as String) as Map; _messageController.add(data); } catch (e) { AppLogger.error('WebSocket: Failed to parse message: $e'); } } void _onError(dynamic error) { AppLogger.error('WebSocket error: $error'); _status = WebSocketStatus.error; _scheduleReconnect(); } void _onDone() { _status = WebSocketStatus.disconnected; _pingTimer?.cancel(); _scheduleReconnect(); } void _scheduleReconnect() { if (_reconnectAttempts >= _maxReconnectAttempts) { return; } _reconnectTimer?.cancel(); _reconnectAttempts++; final delay = _reconnectDelay * _reconnectAttempts; _reconnectTimer = Timer(delay, () { connect(); }); } void _startPingTimer() { _pingTimer?.cancel(); _pingTimer = Timer.periodic(_pingInterval, (timer) { if (_status == WebSocketStatus.connected) { try { _channel?.sink.add(jsonEncode({'type': 'ping'})); } catch (_) {} } }); } void disconnect() { _reconnectTimer?.cancel(); _pingTimer?.cancel(); _subscription?.cancel(); _channel?.sink.close(); _status = WebSocketStatus.disconnected; } void dispose() { disconnect(); _messageController.close(); } }