// ignore_for_file: avoid_init_to_null, prefer_final_fields import 'dart:io'; import 'dart:async'; import 'dart:collection'; import 'dart:typed_data'; import 'package:protobuf/protobuf.dart'; import 'base_client.dart'; import 'inbound_gateway_io.dart'; import 'packets/message_common.pb.dart'; import 'transport.dart'; abstract class WsProtobufClient extends BaseClient { final WebSocket _ws; late final _WebSocketTransport _transport; /// Plain WebSocket connection. static Future connect(String host, int port, {String path = '/'}) => WebSocket.connect('ws://$host:$port$path'); /// Secure WebSocket (WSS) connection. static Future connectSecure(String host, int port, {String path = '/', SecurityContext? context, bool Function(X509Certificate certificate)? onBadCertificate}) { final client = HttpClient(context: context ?? SecurityContext.defaultContext); if (onBadCertificate != null) { client.badCertificateCallback = (certificate, _, __) { return onBadCertificate(certificate); }; } return WebSocket.connect('wss://$host:$port$path', customClient: client); } StreamSubscription? _subscription; final Queue _pending = Queue(); bool _draining = false; WsProtobufClient(this._ws, int heartbeatIntervalTime, int heartbeatWaitTime, Map)> parserMap) { initSelf(this); initHeartbeat(heartbeatIntervalTime, heartbeatWaitTime); _transport = _WebSocketTransport(_ws); isAlive = true; parserMap.addAll({HeartBeat.getDefault().info_.qualifiedMessageName: HeartBeat.fromBuffer}); super.initialize(parserMap, transport: _transport); // TCP 경로와 동일한 gateway-backed receive coordinator를 사용한다. start()는 // _ready completer를 동기 구간에서 설정하므로 spawn 전 첫 프레임도 drop되지 않는다. final gateway = IsolateInboundGateway(); attachInboundGateway(gateway); unawaited(gateway.start()); _subscription = _ws.listen(_onMessage, onError: onError, onDone: close); addListener(onHeartBeat); sendHeartBeat(); } void onError(dynamic e) {} // burst 상황에서 pause()는 동기적으로 적용되지 않아 이미 전달된 frame이 남는다. // 도착한 frame을 drop하지 않고 queue에 넣은 뒤, 단일 drain loop가 순서대로 // dispatch한다. pause/resume은 source backpressure 용도로 drain 단위에서만 건다. void _onMessage(dynamic data) { _pending.add(data); _drain(); } Future _drain() async { if (_draining) return; _draining = true; _subscription?.pause(); try { while (_pending.isNotEmpty) { await _dispatch(_pending.removeFirst()); } } finally { _draining = false; _subscription?.resume(); } } Future _dispatch(dynamic data) async { final bytes = data is List ? data : (data as Uint8List).toList(); await onReceivedFrame(bytes); sendHeartBeat(); } @override Future closeTransport() async { await _transport.close(); } @override bool get isSourcePaused => _subscription?.isPaused ?? false; } class _WebSocketTransport implements Transport { final WebSocket _ws; _WebSocketTransport(this._ws); @override Future writePacket(PacketBase base) async { _ws.add(base.writeToBuffer()); } @override Future close() async { try { await _ws.close(); } catch (_) { // already closed by peer } } }