110 lines
3.1 KiB
Dart
110 lines
3.1 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
import 'package:talker/talker.dart';
|
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
|
|
import '../constants/api_constants.dart';
|
|
import '../constants/app_constants.dart';
|
|
import '../logging/app_logger.dart';
|
|
import '../storage/secure_storage.dart';
|
|
|
|
part 'socket_manager.g.dart';
|
|
|
|
@Riverpod(keepAlive: true)
|
|
SocketManager socketManager(Ref ref) {
|
|
final talker = ref.read(appLoggerProvider);
|
|
final secureStorage = ref.read(secureStorageProvider);
|
|
final manager = SocketManager(talker: talker, secureStorage: secureStorage);
|
|
ref.onDispose(manager.dispose);
|
|
return manager;
|
|
}
|
|
|
|
class SocketManager {
|
|
SocketManager({
|
|
required this.talker,
|
|
required this.secureStorage,
|
|
});
|
|
|
|
final Talker talker;
|
|
final SecureStorage secureStorage;
|
|
|
|
WebSocketChannel? _channel;
|
|
Timer? _reconnectTimer;
|
|
final _messageController = StreamController<Map<String, dynamic>>.broadcast();
|
|
bool _isConnected = false;
|
|
int _reconnectAttempts = 0;
|
|
static const int _maxReconnectAttempts = 5;
|
|
|
|
Stream<Map<String, dynamic>> get messageStream => _messageController.stream;
|
|
bool get isConnected => _isConnected;
|
|
|
|
Future<void> connect({String? path}) async {
|
|
try {
|
|
final token = await secureStorage.read(key: AppConstants.accessTokenKey);
|
|
final wsUrl = '${ApiConstants.wsUrl}${path ?? ''}';
|
|
final uri = Uri.parse(
|
|
token != null ? '$wsUrl?token=$token' : wsUrl,
|
|
);
|
|
|
|
_channel = WebSocketChannel.connect(uri);
|
|
await _channel!.ready;
|
|
_isConnected = true;
|
|
_reconnectAttempts = 0;
|
|
|
|
talker.info('WebSocket connected: $wsUrl');
|
|
|
|
_channel!.stream.listen(
|
|
(data) {
|
|
try {
|
|
final decoded = jsonDecode(data as String) as Map<String, dynamic>;
|
|
_messageController.add(decoded);
|
|
} catch (e) {
|
|
talker.error('WebSocket message parse error', e);
|
|
}
|
|
},
|
|
onError: (Object error) {
|
|
talker.error('WebSocket error', error);
|
|
_handleDisconnect();
|
|
},
|
|
onDone: () {
|
|
talker.warning('WebSocket disconnected');
|
|
_handleDisconnect();
|
|
},
|
|
);
|
|
} catch (e) {
|
|
talker.error('WebSocket connection failed', e);
|
|
_handleDisconnect();
|
|
}
|
|
}
|
|
|
|
void _handleDisconnect() {
|
|
_isConnected = false;
|
|
if (_reconnectAttempts < _maxReconnectAttempts) {
|
|
final delay = Duration(seconds: _reconnectAttempts * 2 + 1);
|
|
_reconnectTimer = Timer(delay, () {
|
|
_reconnectAttempts++;
|
|
talker.info('WebSocket reconnect attempt $_reconnectAttempts');
|
|
connect();
|
|
});
|
|
}
|
|
}
|
|
|
|
void send(Map<String, dynamic> data) {
|
|
if (_isConnected && _channel != null) {
|
|
_channel!.sink.add(jsonEncode(data));
|
|
} else {
|
|
talker.warning('WebSocket not connected, message not sent');
|
|
}
|
|
}
|
|
|
|
void dispose() {
|
|
_reconnectTimer?.cancel();
|
|
_channel?.sink.close();
|
|
_messageController.close();
|
|
_isConnected = false;
|
|
}
|
|
}
|