// ignore_for_file: prefer_final_fields import 'dart:async'; import 'package:meta/meta.dart'; import 'package:protobuf/protobuf.dart'; import 'inbound_gateway.dart'; import 'packets/message_common.pb.dart'; import 'transport.dart'; /// A single frame queued for inbound dispatch. class _InboundItem { final String typeName; final List data; final int incomingNonce; final int responseNonce; const _InboundItem({ required this.typeName, required this.data, required this.incomingNonce, required this.responseNonce, }); } abstract class Communicator { static const int maxNonce = 2147483647; Map _handlerDic = {}; Map Function(String, List, int)> _requestHandlerDic = {}; Map _pendingRequests = {}; late Map)> _instanceGenerator; Map _canonicalNameMap = {}; Future _outboundWrite = Future.value(); Transport? _transport; /// Inbound queue: serial receive worker chained via Future. Future _inboundDispatch = Future.value(); static const int _inboundQueueCapacity = 64; int _inboundQueueLength = 0; bool _isClosing = false; Completer? _queueWaiter; /// Optional worker gateway. When attached, raw frames are decoded off the /// coordinator and reordered by [seq] before reaching [onReceivedData]. /// Defaults to null: frames decode inline on the main isolate. InboundGateway? _inboundGateway; StreamSubscription? _gatewaySubscription; int _frameSeq = 0; /// Frames handed to the gateway but not yet accepted into the dispatch queue. /// /// Decode is never the bottleneck, so the gateway's own in-flight capacity /// cannot throttle a stalled dispatch handler. Gating [onReceivedFrame] on /// this backlog makes a full inbound queue propagate to the transport read /// loop and keeps decoded frames from piling up unbounded behind a stuck /// handler — the same bound the inline path gets for free. int _gatewayBacklog = 0; Completer? _gatewayBacklogWaiter; /// Monotonically increasing nonce. Shared by send / sendRequest / response. int _nonce = 0; int get nonce => _nonce; @protected set nonce(int value) => _nonce = value; @protected int nextNonce() { for (var i = 0; i < maxNonce; i++) { if (_nonce >= maxNonce) { _nonce = 0; } _nonce += 1; if (!_pendingRequests.containsKey(_nonce)) { return _nonce; } } throw StateError('no available nonce: all positive int32 nonces are pending'); } /// Whether the connection is alive. Set by each transport implementation. bool _isAlive = false; bool get isAlive => _isAlive; @protected set isAlive(bool value) => _isAlive = value; Communicator(); void initialize( Map)> instanceGenerator, {required Transport transport}) { final shortToFull = {}; for (final fullName in instanceGenerator.keys) { final shortName = _shortTypeName(fullName); if (shortToFull.containsKey(shortName) && shortToFull[shortName] != fullName) { throw ArgumentError('Duplicate alias mapping: $shortName maps to both ${shortToFull[shortName]} and $fullName'); } shortToFull[shortName] = fullName; } _canonicalNameMap = {}; for (final fullName in instanceGenerator.keys) { _canonicalNameMap[fullName] = fullName; final shortName = _shortTypeName(fullName); _canonicalNameMap[shortName] = fullName; } _instanceGenerator = instanceGenerator; _transport = transport; } String _canonicalize(String typeName) { return _canonicalNameMap[typeName] ?? _canonicalNameMap[_shortTypeName(typeName)] ?? typeName; } T Function(List) getGenerator(String type) { final canonical = _canonicalize(type); final generator = _instanceGenerator[canonical]; if (generator == null) { throw Exception( 'Must set protobuf packet creator before use it. Type: ${_qualifiedMessageNameOf()}'); } return generator as T Function(List); } String _qualifiedMessageNameOf() { final shortName = T.toString(); for (final key in _instanceGenerator.keys) { if (key == shortName || _shortTypeName(key) == shortName) { if (key.contains('.')) { return key; } } } for (final key in _instanceGenerator.keys) { if (key == shortName || _shortTypeName(key) == shortName) { return key; } } return shortName; } String _shortTypeName(String typeName) { final lastDot = typeName.lastIndexOf('.'); if (lastDot < 0 || lastDot == typeName.length - 1) { return typeName; } return typeName.substring(lastDot + 1); } V? _lookupByWireType(Map map, String typeName) { return map[_canonicalize(typeName)]; } GeneratedMessage _decodeMessage( String typeName, List data, { String? fallbackTypeName, }) { final canonical = _canonicalize(typeName); var generator = _instanceGenerator[canonical]; if (generator == null && fallbackTypeName != null) { generator = _instanceGenerator[_canonicalize(fallbackTypeName)]; } if (generator == null) { throw Exception( 'Must set protobuf packet creator before use it. Type: $typeName'); } return generator(data); } /// Serializes writes so stream transports do not interleave packets. Future queuePacket(PacketBase base) { final write = _outboundWrite.then((_) => _transport!.writePacket(base)); _outboundWrite = write.catchError((_) {}); return write; } Future send(T data) async { if (isAlive) { await queuePacket(PacketBase() ..typeName = data.info_.qualifiedMessageName ..nonce = nextNonce() ..data = data.writeToBuffer()); } } @protected void cancelPendingRequests() { final snapshot = Map.from(_pendingRequests); _pendingRequests.clear(); for (final pending in snapshot.values) { pending.completeError( StateError('connection closed'), StackTrace.current); } } /// Sends [data] as a request and waits for a typed response. /// /// The remote side must have registered an [addRequestListener] for [Req]. /// [Res] must be registered in the parser map. /// /// ```dart /// final res = await client.sendRequest(GetUser()..id = 1); /// ``` Future sendRequest( Req data, {Duration timeout = const Duration(seconds: 30)}) async { if (!isAlive) return Future.error(StateError('not connected')); final requestNonce = nextNonce(); final completer = Completer(); final expectedResponseType = _qualifiedMessageNameOf(); _pendingRequests[requestNonce] = _PendingRequest( expectedTypeName: expectedResponseType, complete: (typeName, bytes) { final canonicalExpected = _canonicalize(expectedResponseType); final canonicalReceived = _canonicalize(typeName); if (canonicalReceived != canonicalExpected) { completer.completeError( StateError( 'Response type mismatch for nonce $requestNonce: expected $canonicalExpected, got $typeName'), StackTrace.current, ); return; } final message = _decodeMessage(typeName, bytes, fallbackTypeName: expectedResponseType); if (message is! Res) { completer.completeError( StateError( 'Response type mismatch for nonce $requestNonce: expected $canonicalExpected, got $typeName'), StackTrace.current, ); return; } completer.complete(message); }, completeError: (error, stackTrace) { completer.completeError(error, stackTrace); }, ); await queuePacket(PacketBase() ..typeName = data.info_.qualifiedMessageName ..nonce = requestNonce ..data = data.writeToBuffer()) .catchError((error, stackTrace) { _pendingRequests.remove(requestNonce); completer.completeError(error, stackTrace); }); return completer.future.timeout(timeout, onTimeout: () { _pendingRequests.remove(requestNonce); throw TimeoutException( 'sendRequest timeout for nonce $requestNonce', timeout); }); } /// Registers a request handler for [Req] that returns [Res]. /// /// When a [Req] packet arrives the handler is called and the returned [Res] /// is automatically sent back to the caller. /// /// ```dart /// server.addRequestListener((req) async { /// return UserData()..name = db.getUser(req.id).name; /// }); /// ``` void addRequestListener(Future Function(Req) handler) { final reqType = _qualifiedMessageNameOf(); final canonicalReq = _canonicalize(reqType); if (_handlerDic.containsKey(canonicalReq)) { throw StateError( 'Type $reqType is already registered with addListener and cannot also use addRequestListener.'); } _requestHandlerDic[canonicalReq] = (String typeName, List bytes, int requestNonce) async { final message = _decodeMessage(typeName, bytes, fallbackTypeName: reqType); if (message is! Req) { throw StateError( 'Request type mismatch for nonce $requestNonce: expected ${_canonicalize(reqType)}, got $typeName'); } final req = message; final res = await handler(req); if (isAlive) { await queuePacket(PacketBase() ..typeName = res.info_.qualifiedMessageName ..nonce = nextNonce() ..responseNonce = requestNonce ..data = res.writeToBuffer()); } }; } /// Attaches a worker [gateway] in front of the receive coordinator. /// /// Decoded results are dispatched in input [seq] order, so the coordinator /// keeps its FIFO dispatch and sole ownership of stateful handling. Each /// result releases the backlog slot its frame reserved in [onReceivedFrame], /// only once the frame is actually handled. Attaching is opt-in; without it /// [onReceivedFrame] decodes inline. @protected void attachInboundGateway(InboundGateway gateway) { _inboundGateway = gateway; _gatewaySubscription = gateway.results.listen(_onGatewayResult); } /// Consumes one ordered gateway result and frees its reserved backlog slot /// once handled — not merely decoded — so a stalled handler keeps the /// transport read loop paused via [onReceivedFrame]. void _onGatewayResult(DecodedFrame frame) { if (frame.isError) { onGatewayDecodeError(frame.seq, frame.error!); _releaseGatewayBacklog(); return; } if (frame.responseNonce > 0) { // Responses complete out of band, exactly as the inline coordinator does. final pending = _pendingRequests.remove(frame.responseNonce); pending?.complete(frame.typeName, frame.data); _releaseGatewayBacklog(); return; } // Data/request frames serialize on the dispatch chain. The reserved backlog // slot is held until dispatch completes, so the chain depth — and thus the // outstanding frame bound — matches the inline coordinator's queue. final item = _InboundItem( typeName: frame.typeName, data: frame.data, incomingNonce: frame.incomingNonce, responseNonce: frame.responseNonce, ); _inboundDispatch = _inboundDispatch .then((_) => _dispatchInbound(item)) .whenComplete(_releaseGatewayBacklog); } /// Frees one backlog slot and wakes a waiting [onReceivedFrame] if the gateway /// has fallen back under capacity. void _releaseGatewayBacklog() { if (_gatewayBacklog > 0) _gatewayBacklog--; final waiter = _gatewayBacklogWaiter; if (waiter != null && _gatewayBacklog < _inboundQueueCapacity) { _gatewayBacklogWaiter = null; waiter.complete(); } } /// Handles an ordered decode failure surfaced by the gateway. /// /// A malformed envelope means the inbound framing can no longer be trusted, so /// the connection is torn down and pending requests are cleaned up. Overrides /// (e.g. [BaseClient]) close the transport as part of [close]. @protected void onGatewayDecodeError(int seq, Object error) { if (!isAlive || _isClosing) return; unawaited(close()); } int _nextFrameSeq() { _frameSeq += 1; return _frameSeq; } /// Receives a raw [PacketBase] frame from a transport. /// /// When a gateway is attached the frame is submitted with an internal [seq] /// for off-coordinator decode + reorder; otherwise it is decoded inline and /// forwarded to [onReceivedData]. @protected Future onReceivedFrame(List frame) async { if (!isAlive || _isClosing) return; final gateway = _inboundGateway; if (gateway != null) { // Throttle on the dispatch backlog before handing the frame off. This // propagates a full inbound queue to the transport read loop even though // the gateway's in-flight capacity (decode) is never the bottleneck. while (isAlive && !_isClosing && _gatewayBacklog >= _inboundQueueCapacity) { _gatewayBacklogWaiter ??= Completer(); await _gatewayBacklogWaiter!.future; } if (!isAlive || _isClosing) return; _gatewayBacklog++; // Await so gateway backpressure propagates to the transport read loop. await gateway.submit(InboundFrame(seq: _nextFrameSeq(), bytes: frame)); return; } final common = PacketBase.fromBuffer(frame); await onReceivedData(common.typeName, common.data, incomingNonce: common.nonce, responseNonce: common.responseNonce); } /// Blocks until the inbound dispatch queue has room. Shared by the inline and /// gateway receive paths so a stalled handler throttles the transport equally. Future _awaitInboundCapacity() async { while (_inboundQueueLength >= _inboundQueueCapacity) { _queueWaiter ??= Completer(); await _queueWaiter!.future; } } /// Enqueues an inbound packet for serial dispatch. /// /// The receive worker processes items one at a time, preserving FIFO order /// even when a request handler is async. Future onReceivedData(String typeName, List data, {int incomingNonce = 0, int responseNonce = 0}) async { if (!isAlive || _isClosing) return; if (responseNonce > 0) { final pending = _pendingRequests.remove(responseNonce); if (pending != null) { pending.complete(typeName, data); } return; } await _awaitInboundCapacity(); if (!isAlive || _isClosing) return; _inboundQueueLength++; final item = _InboundItem( typeName: typeName, data: data, incomingNonce: incomingNonce, responseNonce: responseNonce, ); // Chain dispatch onto the previous future to preserve order. _inboundDispatch = _inboundDispatch .then((_) => _dispatchInbound(item)) .whenComplete(() { _inboundQueueLength--; if (_inboundQueueLength < _inboundQueueCapacity && _queueWaiter != null) { final w = _queueWaiter; _queueWaiter = null; w!.complete(); } }); } Future close() async { if (!isAlive || _isClosing) return; _isClosing = true; await _inboundDispatch; isAlive = false; _isClosing = false; if (_queueWaiter != null) { _queueWaiter!.complete(); _queueWaiter = null; } _drainGatewayBacklog(); await _closeInboundGateway(); cancelPendingRequests(); } void shutdown() { if (!isAlive) return; isAlive = false; _isClosing = false; _inboundQueueLength = 0; _inboundDispatch = Future.value(); if (_queueWaiter != null) { _queueWaiter!.complete(); _queueWaiter = null; } _drainGatewayBacklog(); unawaited(_closeInboundGateway()); cancelPendingRequests(); } /// Releases any [onReceivedFrame] blocked on backlog and clears the counter so /// teardown never strands the transport read loop. void _drainGatewayBacklog() { _gatewayBacklog = 0; final waiter = _gatewayBacklogWaiter; if (waiter != null) { _gatewayBacklogWaiter = null; waiter.complete(); } } Future _closeInboundGateway() async { final subscription = _gatewaySubscription; final gateway = _inboundGateway; _gatewaySubscription = null; _inboundGateway = null; await subscription?.cancel(); await gateway?.close(); } /// Dispatches a single inbound item sequentially. Future _dispatchInbound(_InboundItem item) async { if (item.responseNonce > 0) { final pending = _pendingRequests.remove(item.responseNonce); if (pending == null) return; pending.complete(item.typeName, item.data); return; } final canonical = _canonicalize(item.typeName); final requestHandler = _requestHandlerDic[canonical]; if (requestHandler != null) { await requestHandler(item.typeName, item.data, item.incomingNonce); return; } final handler = _handlerDic[canonical]; if (handler != null) { handler.onMessage(item.typeName, item.data); } } void addListener(void Function(T) listener) { var type = _qualifiedMessageNameOf(); var canonical = _canonicalize(type); if (_requestHandlerDic.containsKey(canonical)) { throw StateError( 'Type $type is already registered with addRequestListener and cannot also use addListener.'); } if (!_handlerDic.containsKey(canonical)) { _handlerDic[canonical] = DataHandler((typeName, data) { final message = _decodeMessage(typeName, data, fallbackTypeName: type); if (message is! T) { throw StateError( 'Message type mismatch: expected $canonical, got $typeName'); } return message; }); } var handler = _handlerDic[canonical] as DataHandler; handler.addListener(listener); } void removeListener(void Function(T) listener) { var type = _qualifiedMessageNameOf(); var canonical = _canonicalize(type); if (_handlerDic.containsKey(canonical)) { var handler = _handlerDic[canonical] as DataHandler; handler.removeListener(listener); } } } abstract class IDataHandler { void onMessage(String typeName, List data); } class DataHandler implements IDataHandler { T Function(String, List) _generator; List _listeners = []; DataHandler(this._generator); @override void onMessage(String typeName, List data) { for (var listener in _listeners) { listener.call(_generator(typeName, data)); } } void addListener(void Function(T) handler) { removeListener(handler); // 중복 리스너 불허 _listeners.add(handler); } void removeListener(void Function(T) handler) { _listeners.remove(handler); } } class _PendingRequest { final String expectedTypeName; final void Function(String, List) complete; final void Function(Object, StackTrace) completeError; _PendingRequest({ required this.expectedTypeName, required this.complete, required this.completeError, }); }