297 lines
7.8 KiB
Dart
297 lines
7.8 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
|
|
/// WebSocket service for real-time chat communication
|
|
class ChatWebSocketService {
|
|
static final ChatWebSocketService _instance =
|
|
ChatWebSocketService._internal();
|
|
factory ChatWebSocketService() => _instance;
|
|
ChatWebSocketService._internal();
|
|
|
|
WebSocketChannel? _channel;
|
|
StreamSubscription? _subscription;
|
|
Timer? _pingTimer;
|
|
Timer? _reconnectTimer;
|
|
|
|
bool _isConnected = false;
|
|
bool _isIntentionalClose = false;
|
|
int _reconnectAttempts = 0;
|
|
static const int _maxReconnectAttempts = 5;
|
|
static const Duration _reconnectDelay = Duration(seconds: 3);
|
|
|
|
// Callbacks
|
|
Function(Map<String, dynamic>)? onMessageReceived;
|
|
Function()? onConnected;
|
|
Function()? onDisconnected;
|
|
Function(dynamic error)? onError;
|
|
|
|
String? _currentChatId;
|
|
String? _wsUrl;
|
|
|
|
/// Check if WebSocket is currently connected
|
|
bool get isConnected => _isConnected;
|
|
|
|
/// Get current chat ID
|
|
String? get currentChatId => _currentChatId;
|
|
|
|
/// Connect to WebSocket for a specific chat
|
|
Future<void> connect({
|
|
required String chatId,
|
|
String baseUrl = "wss://taxglide.amrithaa.net:443/reverb",
|
|
String appKey = "2yj0lyzc9ylw2h03ts6i",
|
|
}) async {
|
|
try {
|
|
// Close existing connection if any
|
|
await disconnect();
|
|
|
|
_currentChatId = chatId;
|
|
_wsUrl = "$baseUrl/app/$appKey?protocol=7&client=js&version=1.0";
|
|
_isIntentionalClose = false;
|
|
_reconnectAttempts = 0;
|
|
|
|
await _establishConnection();
|
|
} catch (e) {
|
|
debugPrint("❌ Connection error: $e");
|
|
onError?.call(e);
|
|
_scheduleReconnect();
|
|
}
|
|
}
|
|
|
|
/// Establish WebSocket connection
|
|
Future<void> _establishConnection() async {
|
|
try {
|
|
debugPrint("🔌 Connecting to: $_wsUrl");
|
|
|
|
_channel = WebSocketChannel.connect(Uri.parse(_wsUrl!));
|
|
|
|
// Listen to messages
|
|
_subscription = _channel!.stream.listen(
|
|
_handleMessage,
|
|
onError: _handleError,
|
|
onDone: _handleDisconnection,
|
|
cancelOnError: false,
|
|
);
|
|
|
|
// Wait a bit for connection to establish
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
|
|
_isConnected = true;
|
|
_reconnectAttempts = 0;
|
|
debugPrint("✅ WebSocket connected");
|
|
|
|
// Subscribe to chat channel
|
|
_subscribeToChannel();
|
|
|
|
// Start ping timer
|
|
_startPingTimer();
|
|
|
|
onConnected?.call();
|
|
} catch (e) {
|
|
debugPrint("❌ Failed to establish connection: $e");
|
|
_isConnected = false;
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Handle incoming messages
|
|
void _handleMessage(dynamic data) {
|
|
try {
|
|
debugPrint("📨 Received: $data");
|
|
|
|
final Map<String, dynamic> message = json.decode(data);
|
|
|
|
// Handle Pusher ping
|
|
if (message['event'] == 'pusher:ping') {
|
|
_sendPong();
|
|
return;
|
|
}
|
|
|
|
// Handle connection established
|
|
if (message['event'] == 'pusher:connection_established') {
|
|
debugPrint("🔗 Connection established");
|
|
return;
|
|
}
|
|
|
|
// Handle subscription succeeded
|
|
if (message['event'] == 'pusher_internal:subscription_succeeded') {
|
|
debugPrint("✅ Subscribed to chat.${_currentChatId}");
|
|
return;
|
|
}
|
|
|
|
// Handle custom events (your chat messages)
|
|
if (message['event'] != null &&
|
|
!message['event'].toString().startsWith('pusher')) {
|
|
onMessageReceived?.call(message);
|
|
}
|
|
} catch (e) {
|
|
debugPrint("❌ Error parsing message: $e");
|
|
onError?.call(e);
|
|
}
|
|
}
|
|
|
|
/// Handle WebSocket errors
|
|
void _handleError(dynamic error) {
|
|
debugPrint("❌ WebSocket error: $error");
|
|
_isConnected = false;
|
|
onError?.call(error);
|
|
|
|
if (!_isIntentionalClose) {
|
|
_scheduleReconnect();
|
|
}
|
|
}
|
|
|
|
/// Handle disconnection
|
|
void _handleDisconnection() {
|
|
debugPrint("🔌 WebSocket disconnected");
|
|
_isConnected = false;
|
|
_stopPingTimer();
|
|
onDisconnected?.call();
|
|
|
|
if (!_isIntentionalClose) {
|
|
_scheduleReconnect();
|
|
}
|
|
}
|
|
|
|
/// Subscribe to chat channel
|
|
void _subscribeToChannel() {
|
|
if (_currentChatId == null) return;
|
|
|
|
final subscribeMessage = {
|
|
'event': 'pusher:subscribe',
|
|
'data': {'channel': 'chat.$_currentChatId'},
|
|
};
|
|
|
|
_sendMessage(subscribeMessage);
|
|
debugPrint("📡 Subscribing to chat.$_currentChatId");
|
|
}
|
|
|
|
/// Send pong response to ping
|
|
void _sendPong() {
|
|
final pongMessage = {'event': 'pusher:pong', 'data': {}};
|
|
_sendMessage(pongMessage);
|
|
debugPrint("🏓 Pong sent");
|
|
}
|
|
|
|
/// Start periodic ping timer
|
|
void _startPingTimer() {
|
|
_pingTimer?.cancel();
|
|
_pingTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
|
|
if (_isConnected) {
|
|
// Server will send ping, we just respond with pong
|
|
debugPrint("💓 Heartbeat check");
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Stop ping timer
|
|
void _stopPingTimer() {
|
|
_pingTimer?.cancel();
|
|
_pingTimer = null;
|
|
}
|
|
|
|
/// Schedule reconnection attempt
|
|
void _scheduleReconnect() {
|
|
if (_isIntentionalClose) return;
|
|
if (_reconnectAttempts >= _maxReconnectAttempts) {
|
|
debugPrint("❌ Max reconnection attempts reached");
|
|
onError?.call(
|
|
"Failed to reconnect after $_maxReconnectAttempts attempts",
|
|
);
|
|
return;
|
|
}
|
|
|
|
_reconnectTimer?.cancel();
|
|
_reconnectAttempts++;
|
|
|
|
debugPrint(
|
|
"🔄 Scheduling reconnect attempt $_reconnectAttempts in ${_reconnectDelay.inSeconds}s",
|
|
);
|
|
|
|
_reconnectTimer = Timer(_reconnectDelay, () async {
|
|
if (!_isIntentionalClose && _wsUrl != null && _currentChatId != null) {
|
|
debugPrint("🔄 Attempting to reconnect...");
|
|
try {
|
|
await _establishConnection();
|
|
} catch (e) {
|
|
debugPrint("❌ Reconnect failed: $e");
|
|
_scheduleReconnect();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Send a message through WebSocket
|
|
void _sendMessage(Map<String, dynamic> message) {
|
|
if (_channel == null || !_isConnected) {
|
|
debugPrint("⚠️ Cannot send message: Not connected");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final jsonMessage = json.encode(message);
|
|
_channel!.sink.add(jsonMessage);
|
|
debugPrint("📤 Sent: $jsonMessage");
|
|
} catch (e) {
|
|
debugPrint("❌ Error sending message: $e");
|
|
onError?.call(e);
|
|
}
|
|
}
|
|
|
|
/// Send a custom event to the channel
|
|
void sendEvent({
|
|
required String eventName,
|
|
required Map<String, dynamic> data,
|
|
}) {
|
|
final message = {
|
|
'event': eventName,
|
|
'data': data,
|
|
'channel': 'chat.$_currentChatId',
|
|
};
|
|
_sendMessage(message);
|
|
}
|
|
|
|
/// Disconnect from WebSocket
|
|
Future<void> disconnect() async {
|
|
_isIntentionalClose = true;
|
|
_reconnectTimer?.cancel();
|
|
_stopPingTimer();
|
|
|
|
await _subscription?.cancel();
|
|
await _channel?.sink.close();
|
|
|
|
_channel = null;
|
|
_subscription = null;
|
|
_isConnected = false;
|
|
_currentChatId = null;
|
|
|
|
debugPrint("🔌 WebSocket disconnected intentionally");
|
|
}
|
|
|
|
/// Switch to a different chat channel
|
|
Future<void> switchChannel(String newChatId) async {
|
|
if (newChatId == _currentChatId) return;
|
|
|
|
debugPrint("🔄 Switching from chat.$_currentChatId to chat.$newChatId");
|
|
|
|
// Unsubscribe from current channel
|
|
if (_currentChatId != null) {
|
|
final unsubscribeMessage = {
|
|
'event': 'pusher:unsubscribe',
|
|
'data': {'channel': 'chat.$_currentChatId'},
|
|
};
|
|
_sendMessage(unsubscribeMessage);
|
|
}
|
|
|
|
// Update chat ID and subscribe to new channel
|
|
_currentChatId = newChatId;
|
|
_subscribeToChannel();
|
|
}
|
|
|
|
/// Dispose and cleanup
|
|
void dispose() {
|
|
disconnect();
|
|
}
|
|
}
|