82 lines
2.3 KiB
Dart
82 lines
2.3 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
|
|
import '../config.dart';
|
|
|
|
enum WsStatus { connecting, connected, disconnected }
|
|
|
|
/// کلاینت WebSocket بازی: اتصال با توکن، ارسال/دریافت JSON و اتصال مجدد خودکار.
|
|
class WsClient {
|
|
final String token;
|
|
WebSocketChannel? _channel;
|
|
StreamSubscription? _sub;
|
|
Timer? _reconnectTimer;
|
|
bool _disposed = false;
|
|
int _attempt = 0;
|
|
|
|
final _messages = StreamController<Map<String, dynamic>>.broadcast();
|
|
final _status = StreamController<WsStatus>.broadcast();
|
|
|
|
WsClient(this.token);
|
|
|
|
/// پیامهای دیکدشدهی سرور.
|
|
Stream<Map<String, dynamic>> get messages => _messages.stream;
|
|
|
|
/// وضعیت اتصال (برای نمایش «در حال اتصال مجدد»).
|
|
Stream<WsStatus> get status => _status.stream;
|
|
|
|
void connect() {
|
|
if (_disposed) return;
|
|
_status.add(WsStatus.connecting);
|
|
try {
|
|
final ch = WebSocketChannel.connect(Uri.parse(AppConfig.wsUrl(token)));
|
|
_channel = ch;
|
|
_sub = ch.stream.listen(
|
|
_onData,
|
|
onError: (_) => _scheduleReconnect(),
|
|
onDone: _scheduleReconnect,
|
|
cancelOnError: true,
|
|
);
|
|
_attempt = 0;
|
|
_status.add(WsStatus.connected);
|
|
} catch (_) {
|
|
_scheduleReconnect();
|
|
}
|
|
}
|
|
|
|
void _onData(dynamic raw) {
|
|
try {
|
|
final msg = jsonDecode(raw as String);
|
|
if (msg is Map<String, dynamic>) _messages.add(msg);
|
|
} catch (_) {/* پیام نامعتبر نادیده گرفته میشود */}
|
|
}
|
|
|
|
void send(Map<String, dynamic> msg) {
|
|
_channel?.sink.add(jsonEncode(msg));
|
|
}
|
|
|
|
void _scheduleReconnect() {
|
|
if (_disposed) return;
|
|
_status.add(WsStatus.disconnected);
|
|
_sub?.cancel();
|
|
_sub = null;
|
|
_channel = null;
|
|
_reconnectTimer?.cancel();
|
|
// backoff تا حداکثر ۵ ثانیه؛ سرور با همان توکن بازیکن را به میز برمیگرداند.
|
|
final delayMs = (500 * (1 << _attempt)).clamp(500, 5000);
|
|
_attempt = (_attempt + 1).clamp(0, 4);
|
|
_reconnectTimer = Timer(Duration(milliseconds: delayMs), connect);
|
|
}
|
|
|
|
void dispose() {
|
|
_disposed = true;
|
|
_reconnectTimer?.cancel();
|
|
_sub?.cancel();
|
|
_channel?.sink.close();
|
|
_messages.close();
|
|
_status.close();
|
|
}
|
|
}
|