import 'dart:async'; import 'dart:collection'; import 'dart:isolate'; import 'package:uuid/uuid.dart'; import '../../angel3_pub_sub.dart'; /// A [Client] implementation that communicates via [SendPort]s and [ReceivePort]s. class IsolateClient extends Client { final Queue<Completer<String>> _onConnect = Queue<Completer<String>>(); final Map<String, Completer<Map>> _requests = {}; final List<_IsolateClientSubscription> _subscriptions = []; final Uuid _uuid = Uuid(); String? _id; /// The ID of the client we are authenticating as. /// /// May be `null`, if and only if we are marked as a trusted source on /// the server side. String? get clientId => _clientId; String? _clientId; /// A server's [SendPort] that messages should be sent to. final SendPort serverSendPort; /// A [ReceivePort] that receives messages from the server. final ReceivePort receivePort = ReceivePort(); IsolateClient(String? clientId, this.serverSendPort) { _clientId = clientId; receivePort.listen((data) { if (data is Map && data['request_id'] is String) { var requestId = data['request_id'] as String?; var c = _requests.remove(requestId); if (c != null && !c.isCompleted) { if (data['status'] is! bool) { c.completeError( FormatException('The server sent an invalid response.')); } else if (!(data['status'] as bool)) { c.completeError(PubSubException(data['error_message']?.toString() ?? 'The server sent a failure response, but did not provide an error message.')); } else if (data['result'] is! Map) { c.completeError(FormatException( 'The server sent a success response, but did not include a result.')); } else { c.complete(data['result'] as Map?); } } } else if (data is Map && data['id'] is String && _id == null) { _id = data['id'] as String?; for (var c in _onConnect) { if (!c.isCompleted) c.complete(_id); } _onConnect.clear(); } else if (data is List && data.length == 2 && data[0] is String) { var eventName = data[0] as String; var event = data[1]; for (var s in _subscriptions.where((s) => s.eventName == eventName)) { if (!s._stream.isClosed) s._stream.add(event); } } }); serverSendPort.send(receivePort.sendPort); } Future<T> _whenConnected<T>(FutureOr<T> Function() callback) { if (_id != null) { return Future<T>.sync(callback); } else { var c = Completer<String>(); _onConnect.add(c); return c.future.then<T>((_) => callback()); } } @override Future publish(String eventName, value) { return _whenConnected(() { var c = Completer<Map>(); var requestId = _uuid.v4(); _requests[requestId] = c; serverSendPort.send({ 'id': _id, 'request_id': requestId, 'method': 'publish', 'params': { 'client_id': clientId, 'event_name': eventName, 'value': value } }); return c.future.then((result) { _clientId = result['client_id'] as String?; }); }); } @override Future<ClientSubscription> subscribe(String eventName) { return _whenConnected<ClientSubscription>(() { var c = Completer<Map>(); var requestId = _uuid.v4(); _requests[requestId] = c; serverSendPort.send({ 'id': _id, 'request_id': requestId, 'method': 'subscribe', 'params': {'client_id': clientId, 'event_name': eventName} }); return c.future.then<ClientSubscription>((result) { _clientId = result['client_id'] as String?; var s = _IsolateClientSubscription( eventName, result['subscription_id'] as String?, this); _subscriptions.add(s); return s; }); }); } @override Future close() { receivePort.close(); for (var c in _onConnect) { if (!c.isCompleted) { c.completeError(StateError( 'The client was closed before the server ever accepted the connection.')); } } for (var c in _requests.values) { if (!c.isCompleted) { c.completeError(StateError( 'The client was closed before the server responded to this request.')); } } for (var s in _subscriptions) { s._close(); } _requests.clear(); return Future.value(); } } class _IsolateClientSubscription extends ClientSubscription { final StreamController _stream = StreamController(); final String? eventName, id; final IsolateClient client; _IsolateClientSubscription(this.eventName, this.id, this.client); void _close() { if (!_stream.isClosed) _stream.close(); } @override StreamSubscription listen(void Function(dynamic event)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) { return _stream.stream.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } @override Future unsubscribe() { return client._whenConnected(() { var c = Completer<Map>(); var requestId = client._uuid.v4(); client._requests[requestId] = c; client.serverSendPort.send({ 'id': client._id, 'request_id': requestId, 'method': 'unsubscribe', 'params': {'client_id': client.clientId, 'subscription_id': id} }); return c.future.then((_) { _close(); }); }); } }