2019-04-18 02:02:27 +00:00
|
|
|
import 'dart:async';
|
|
|
|
import 'remote_client.dart';
|
|
|
|
import 'transport.dart';
|
|
|
|
|
|
|
|
abstract class Server {
|
|
|
|
final RemoteClient client;
|
2019-04-18 04:12:52 +00:00
|
|
|
final Duration keepAliveInterval;
|
2019-04-18 02:02:27 +00:00
|
|
|
final Completer _done = Completer();
|
|
|
|
StreamSubscription<OperationMessage> _sub;
|
|
|
|
bool _init = false;
|
2019-04-19 02:02:42 +00:00
|
|
|
Timer _timer;
|
2019-04-18 02:02:27 +00:00
|
|
|
|
|
|
|
Future get done => _done.future;
|
|
|
|
|
2019-04-18 04:12:52 +00:00
|
|
|
Server(this.client, {this.keepAliveInterval}) {
|
|
|
|
_sub = client.stream.listen(
|
|
|
|
(msg) async {
|
|
|
|
if ((msg.type == OperationMessage.gqlConnectionInit) && !_init) {
|
|
|
|
try {
|
2019-04-24 17:06:14 +00:00
|
|
|
Map connectionParams;
|
2019-08-14 04:48:18 +00:00
|
|
|
if (msg.payload is Map) {
|
2019-04-18 04:12:52 +00:00
|
|
|
connectionParams = msg.payload as Map;
|
2019-08-14 04:48:18 +00:00
|
|
|
} else if (msg.payload != null) {
|
2019-04-18 04:12:52 +00:00
|
|
|
throw FormatException(
|
|
|
|
'${msg.type} payload must be a map (object).');
|
2019-08-14 04:48:18 +00:00
|
|
|
}
|
2019-04-18 02:02:27 +00:00
|
|
|
|
2019-04-18 04:12:52 +00:00
|
|
|
var connect = await onConnect(client, connectionParams);
|
|
|
|
if (!connect) throw false;
|
|
|
|
_init = true;
|
|
|
|
client.sink
|
|
|
|
.add(OperationMessage(OperationMessage.gqlConnectionAck));
|
2019-04-18 02:02:27 +00:00
|
|
|
|
2019-04-19 02:02:42 +00:00
|
|
|
if (keepAliveInterval != null) {
|
|
|
|
client.sink.add(
|
|
|
|
OperationMessage(OperationMessage.gqlConnectionKeepAlive));
|
|
|
|
_timer ??= Timer.periodic(keepAliveInterval, (timer) {
|
|
|
|
client.sink.add(OperationMessage(
|
|
|
|
OperationMessage.gqlConnectionKeepAlive));
|
|
|
|
});
|
|
|
|
}
|
2019-04-18 04:12:52 +00:00
|
|
|
} catch (e) {
|
2019-08-14 04:48:18 +00:00
|
|
|
if (e == false) {
|
2019-04-18 04:12:52 +00:00
|
|
|
_reportError('The connection was rejected.');
|
2019-08-14 04:48:18 +00:00
|
|
|
} else {
|
2019-04-18 04:12:52 +00:00
|
|
|
_reportError(e.toString());
|
2019-08-14 04:48:18 +00:00
|
|
|
}
|
2019-04-18 02:02:27 +00:00
|
|
|
}
|
2019-04-18 04:12:52 +00:00
|
|
|
} else if (_init) {
|
|
|
|
if (msg.type == OperationMessage.gqlStart) {
|
2019-08-14 04:48:18 +00:00
|
|
|
if (msg.id == null) {
|
2019-04-18 04:12:52 +00:00
|
|
|
throw FormatException('${msg.type} id is required.');
|
2019-08-14 04:48:18 +00:00
|
|
|
}
|
|
|
|
if (msg.payload == null) {
|
2019-04-18 04:12:52 +00:00
|
|
|
throw FormatException('${msg.type} payload is required.');
|
2019-08-14 04:48:18 +00:00
|
|
|
} else if (msg.payload is! Map) {
|
2019-04-18 04:12:52 +00:00
|
|
|
throw FormatException(
|
|
|
|
'${msg.type} payload must be a map (object).');
|
2019-08-14 04:48:18 +00:00
|
|
|
}
|
2019-04-18 04:12:52 +00:00
|
|
|
var payload = msg.payload as Map;
|
|
|
|
var query = payload['query'];
|
|
|
|
var variables = payload['variables'];
|
|
|
|
var operationName = payload['operationName'];
|
2019-08-14 04:48:18 +00:00
|
|
|
if (query == null || query is! String) {
|
2019-04-18 04:12:52 +00:00
|
|
|
throw FormatException(
|
|
|
|
'${msg.type} payload must contain a string named "query".');
|
2019-08-14 04:48:18 +00:00
|
|
|
}
|
|
|
|
if (variables != null && variables is! Map) {
|
2019-04-18 04:12:52 +00:00
|
|
|
throw FormatException(
|
|
|
|
'${msg.type} payload\'s "variables" field must be a map (object).');
|
2019-08-14 04:48:18 +00:00
|
|
|
}
|
|
|
|
if (operationName != null && operationName is! String) {
|
2019-04-18 04:12:52 +00:00
|
|
|
throw FormatException(
|
|
|
|
'${msg.type} payload\'s "operationName" field must be a string.');
|
2019-08-14 04:48:18 +00:00
|
|
|
}
|
2019-04-18 04:12:52 +00:00
|
|
|
var result = await onOperation(
|
|
|
|
msg.id,
|
|
|
|
query as String,
|
|
|
|
(variables as Map)?.cast<String, dynamic>(),
|
|
|
|
operationName as String);
|
|
|
|
var data = result.data;
|
|
|
|
|
2019-04-19 02:02:42 +00:00
|
|
|
if (result.errors.isNotEmpty) {
|
|
|
|
client.sink.add(OperationMessage(OperationMessage.gqlData,
|
|
|
|
id: msg.id, payload: {'errors': result.errors.toList()}));
|
|
|
|
} else {
|
|
|
|
if (data is Map &&
|
|
|
|
data.keys.length == 1 &&
|
|
|
|
data.containsKey('data')) {
|
|
|
|
data = data['data'];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (data is Stream) {
|
|
|
|
await for (var event in data) {
|
|
|
|
if (event is Map &&
|
|
|
|
event.keys.length == 1 &&
|
|
|
|
event.containsKey('data')) {
|
|
|
|
event = event['data'];
|
|
|
|
}
|
|
|
|
client.sink.add(OperationMessage(OperationMessage.gqlData,
|
|
|
|
id: msg.id, payload: {'data': event}));
|
|
|
|
}
|
|
|
|
} else {
|
2019-04-18 04:12:52 +00:00
|
|
|
client.sink.add(OperationMessage(OperationMessage.gqlData,
|
2019-04-19 02:02:42 +00:00
|
|
|
id: msg.id, payload: {'data': data}));
|
2019-04-18 04:12:52 +00:00
|
|
|
}
|
|
|
|
}
|
2019-04-18 02:02:27 +00:00
|
|
|
|
2019-04-18 04:12:52 +00:00
|
|
|
// c.complete();
|
|
|
|
client.sink.add(
|
|
|
|
OperationMessage(OperationMessage.gqlComplete, id: msg.id));
|
2019-04-19 02:02:42 +00:00
|
|
|
} else if (msg.type == OperationMessage.gqlConnectionTerminate) {
|
|
|
|
await _sub?.cancel();
|
2019-04-18 04:12:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
onError: _done.completeError,
|
|
|
|
onDone: () {
|
|
|
|
_done.complete();
|
2019-04-19 02:02:42 +00:00
|
|
|
_timer?.cancel();
|
2019-04-18 04:12:52 +00:00
|
|
|
});
|
2019-04-18 02:02:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void _reportError(String message) {
|
|
|
|
client.sink.add(OperationMessage(OperationMessage.gqlConnectionError,
|
|
|
|
payload: {'message': message}));
|
|
|
|
}
|
|
|
|
|
|
|
|
FutureOr<bool> onConnect(RemoteClient client, [Map connectionParams]);
|
|
|
|
|
|
|
|
FutureOr<GraphQLResult> onOperation(String id, String query,
|
|
|
|
[Map<String, dynamic> variables, String operationName]);
|
|
|
|
}
|