init
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../config.dart';
|
||||
import '../storage/token_storage.dart';
|
||||
|
||||
/// کلاینت HTTP با تزریق خودکار توکن Bearer.
|
||||
class ApiClient {
|
||||
final Dio dio;
|
||||
|
||||
ApiClient(TokenStorage storage)
|
||||
: dio = Dio(BaseOptions(
|
||||
baseUrl: AppConfig.apiUrl,
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
headers: {'Accept': 'application/json'},
|
||||
)) {
|
||||
dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
final token = await storage.read();
|
||||
if (token != null && token.isNotEmpty) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
handler.next(options);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user