116 lines
3.7 KiB
Dart
116 lines
3.7 KiB
Dart
import 'dart:async';
|
||
import 'dart:convert';
|
||
import 'package:chepuhagram/domain/services/api_service.dart';
|
||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||
import 'package:web_socket_channel/status.dart' as status;
|
||
import 'package:web_socket_channel/io.dart';
|
||
import 'package:chepuhagram/core/constants.dart';
|
||
import 'package:flutter/widgets.dart';
|
||
|
||
class SocketService with WidgetsBindingObserver {
|
||
|
||
static final SocketService _instance = SocketService._internal();
|
||
factory SocketService() => _instance;
|
||
|
||
SocketService._internal() {
|
||
WidgetsBinding.instance.addObserver(this);
|
||
}
|
||
|
||
WebSocketChannel? _channel;
|
||
final StreamController<Map<String, dynamic>> _messageController =
|
||
StreamController<Map<String, dynamic>>.broadcast();
|
||
|
||
// Поток, который будут слушать провайдеры
|
||
Stream<Map<String, dynamic>> get messages => _messageController.stream;
|
||
|
||
bool allowConnect = true; // Флаг для контроля подключения
|
||
|
||
|
||
@override
|
||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||
if (state == AppLifecycleState.resumed) {
|
||
allowConnect = true;
|
||
} else {
|
||
allowConnect = false;
|
||
disconnect();
|
||
}
|
||
}
|
||
|
||
Future<void> connect(ApiService apiService) async {
|
||
final token = await apiService.getAccessToken();
|
||
if (_channel != null) return; // Уже подключены
|
||
if (token == null || token.isEmpty) {
|
||
print('❌ SocketService.connect: no access token, skipping connect');
|
||
return;
|
||
}
|
||
if (!allowConnect) return; // Не разрешаем подключение
|
||
|
||
// В FastAPI эндпоинт ожидает токен в URL-параметре
|
||
final uri = Uri.parse("${AppConstants.wsUrl}/ws?token=$token");
|
||
|
||
//_channel = WebSocketChannel.connect(uri);
|
||
|
||
_channel = IOWebSocketChannel.connect(
|
||
uri,
|
||
connectTimeout: Duration(seconds: 10),
|
||
);
|
||
|
||
try {
|
||
await _channel!.ready;
|
||
_channel!.stream.listen(
|
||
(data) {
|
||
final decoded = jsonDecode(data);
|
||
print("🚀 СООБЩЕНИЕ ПОЛУЧЕНО ИЗ SINK: $decoded");
|
||
_messageController.add(decoded);
|
||
},
|
||
onError: (error) => _reconnect(apiService),
|
||
onDone: () => _reconnect(apiService),
|
||
);
|
||
} on TimeoutException catch (_) {
|
||
_channel = null;
|
||
throw Exception('timeout');
|
||
} catch (e) {
|
||
_channel = null;
|
||
throw Exception("Ошибка подключения: $e");
|
||
}
|
||
}
|
||
|
||
Future<void> _reconnect(ApiService apiService) async {
|
||
_channel = null;
|
||
Future.delayed(const Duration(seconds: 5), () => connect(apiService));
|
||
}
|
||
|
||
bool sendMessage(Map<String, dynamic> data, {int retryCnt = 0}) {
|
||
const maxRetries = 5;
|
||
if (_channel == null) {
|
||
if (retryCnt < maxRetries) {
|
||
// Schedule retry with exponential backoff
|
||
Future.delayed(Duration(seconds: 1 << retryCnt), () => sendMessage(data, retryCnt: retryCnt + 1));
|
||
}
|
||
return false;
|
||
}
|
||
try {
|
||
final encodedData = jsonEncode(data);
|
||
|
||
// 1. Проверяем, не закрыт ли sink (у некоторых провайдеров это доступно)
|
||
_channel!.sink.add(encodedData);
|
||
|
||
// 2. Добавляем принт подтверждения
|
||
print("🚀 СООБЩЕНИЕ ОТПРАВЛЕНО В SINK: $encodedData");
|
||
return true;
|
||
} catch (e) {
|
||
print("❌ КРИТИЧЕСКАЯ ОШИБКА ПРИ ОТПРАВКЕ: $e");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
bool sendReadReceipt(int messageId) {
|
||
return sendMessage({'type': 'read_receipt', 'message_id': messageId});
|
||
}
|
||
|
||
void disconnect() {
|
||
_channel?.sink.close(status.normalClosure);
|
||
_channel = null;
|
||
}
|
||
}
|