Files
2026-06-15 16:42:28 +03:30

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();
}
}