proto-socket/dart/lib/src/communicator.dart

581 lines
19 KiB
Dart

// 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<int> 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<String, IDataHandler> _handlerDic = {};
Map<String, Future<void> Function(String, List<int>, int)>
_requestHandlerDic = {};
Map<int, _PendingRequest> _pendingRequests = {};
late Map<String, GeneratedMessage Function(List<int>)> _instanceGenerator;
Map<String, String> _canonicalNameMap = {};
Future<void> _outboundWrite = Future.value();
Transport? _transport;
/// Inbound queue: serial receive worker chained via Future.
Future<void> _inboundDispatch = Future.value();
static const int _inboundQueueCapacity = 64;
int _inboundQueueLength = 0;
bool _isClosing = false;
Completer<void>? _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<DecodedFrame>? _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<void>? _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<String, GeneratedMessage Function(List<int>)> instanceGenerator,
{required Transport transport}) {
final shortToFull = <String, String>{};
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<int>) getGenerator<T extends GeneratedMessage>(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<T>()}');
}
return generator as T Function(List<int>);
}
String _qualifiedMessageNameOf<T extends GeneratedMessage>() {
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<V>(Map<String, V> map, String typeName) {
return map[_canonicalize(typeName)];
}
GeneratedMessage _decodeMessage(
String typeName,
List<int> 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<void> queuePacket(PacketBase base) {
final write = _outboundWrite.then((_) => _transport!.writePacket(base));
_outboundWrite = write.catchError((_) {});
return write;
}
Future<void> send<T extends GeneratedMessage>(T data) async {
if (isAlive) {
await queuePacket(PacketBase()
..typeName = data.info_.qualifiedMessageName
..nonce = nextNonce()
..data = data.writeToBuffer());
}
}
@protected
void cancelPendingRequests() {
final snapshot = Map<int, _PendingRequest>.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, UserData>(GetUser()..id = 1);
/// ```
Future<Res>
sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
Req data,
{Duration timeout = const Duration(seconds: 30)}) async {
if (!isAlive) return Future.error(StateError('not connected'));
final requestNonce = nextNonce();
final completer = Completer<Res>();
final expectedResponseType = _qualifiedMessageNameOf<Res>();
_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<GetUser, UserData>((req) async {
/// return UserData()..name = db.getUser(req.id).name;
/// });
/// ```
void addRequestListener<Req extends GeneratedMessage,
Res extends GeneratedMessage>(Future<Res> Function(Req) handler) {
final reqType = _qualifiedMessageNameOf<Req>();
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<int> 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<void> onReceivedFrame(List<int> 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<void>();
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<void> _awaitInboundCapacity() async {
while (_inboundQueueLength >= _inboundQueueCapacity) {
_queueWaiter ??= Completer<void>();
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<void> onReceivedData(String typeName, List<int> 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<void> 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<void> _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<void> _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<T extends GeneratedMessage>(void Function(T) listener) {
var type = _qualifiedMessageNameOf<T>();
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<T>((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<T>;
handler.addListener(listener);
}
void removeListener<T extends GeneratedMessage>(void Function(T) listener) {
var type = _qualifiedMessageNameOf<T>();
var canonical = _canonicalize(type);
if (_handlerDic.containsKey(canonical)) {
var handler = _handlerDic[canonical] as DataHandler<T>;
handler.removeListener(listener);
}
}
}
abstract class IDataHandler {
void onMessage(String typeName, List<int> data);
}
class DataHandler<T extends GeneratedMessage> implements IDataHandler {
T Function(String, List<int>) _generator;
List<void Function(T)> _listeners = [];
DataHandler(this._generator);
@override
void onMessage(String typeName, List<int> 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<int>) complete;
final void Function(Object, StackTrace) completeError;
_PendingRequest({
required this.expectedTypeName,
required this.complete,
required this.completeError,
});
}