581 lines
19 KiB
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,
|
|
});
|
|
}
|