124 lines
3.4 KiB
Dart
124 lines
3.4 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
|
|
import 'signalr_protocol.dart';
|
|
import 'text_message_format.dart';
|
|
|
|
/// Active SignalR connection for the questions hub.
|
|
class QuestionsHubConnection {
|
|
QuestionsHubConnection({
|
|
required this.connectionToken,
|
|
required this.firebaseUid,
|
|
required this.channel,
|
|
});
|
|
|
|
final String connectionToken;
|
|
final String firebaseUid;
|
|
final WebSocketChannel channel;
|
|
|
|
bool handshakeComplete = false;
|
|
StreamSubscription<Object?>? _subscription;
|
|
|
|
void listen(void Function(String message) onMessage, {void Function()? onDone}) {
|
|
_subscription = channel.stream.listen(
|
|
(Object? message) {
|
|
if (message is String) {
|
|
onMessage(message);
|
|
}
|
|
},
|
|
onDone: onDone,
|
|
cancelOnError: true,
|
|
);
|
|
}
|
|
|
|
Future<void> sendRaw(String payload) async {
|
|
channel.sink.add(payload);
|
|
}
|
|
|
|
Future<void> sendInvocation(String target, List<Object?> arguments) async {
|
|
await sendRaw(SignalrProtocol.invocation(
|
|
target: target,
|
|
arguments: arguments,
|
|
));
|
|
}
|
|
|
|
Future<void> close() async {
|
|
await _subscription?.cancel();
|
|
await channel.sink.close();
|
|
}
|
|
}
|
|
|
|
/// Tracks connected clients and delivers hub invocations by Firebase UID.
|
|
class QuestionsHubConnections {
|
|
final Map<String, QuestionsHubConnection> _byToken =
|
|
<String, QuestionsHubConnection>{};
|
|
final Map<String, Set<String>> _tokensByUid = <String, Set<String>>{};
|
|
|
|
void register(QuestionsHubConnection connection) {
|
|
_byToken[connection.connectionToken] = connection;
|
|
_tokensByUid
|
|
.putIfAbsent(connection.firebaseUid, () => <String>{})
|
|
.add(connection.connectionToken);
|
|
}
|
|
|
|
Future<void> unregister(String connectionToken) async {
|
|
final QuestionsHubConnection? connection = _byToken.remove(connectionToken);
|
|
if (connection == null) {
|
|
return;
|
|
}
|
|
final Set<String>? tokens = _tokensByUid[connection.firebaseUid];
|
|
tokens?.remove(connectionToken);
|
|
if (tokens != null && tokens.isEmpty) {
|
|
_tokensByUid.remove(connection.firebaseUid);
|
|
}
|
|
await connection.close();
|
|
}
|
|
|
|
bool isConnected(String firebaseUid) =>
|
|
(_tokensByUid[firebaseUid]?.isNotEmpty ?? false);
|
|
|
|
Future<void> pushQuestionToConnection(
|
|
QuestionsHubConnection connection,
|
|
Map<String, dynamic> question,
|
|
) async {
|
|
if (!connection.handshakeComplete) {
|
|
return;
|
|
}
|
|
await connection.sendInvocation('ReceiveQuestion', <Object?>[question]);
|
|
}
|
|
|
|
Future<int> pushQuestion(
|
|
String firebaseUid,
|
|
Map<String, dynamic> question,
|
|
) async {
|
|
final Set<String> tokens = _tokensByUid[firebaseUid] ?? <String>{};
|
|
var delivered = 0;
|
|
for (final String token in tokens) {
|
|
final QuestionsHubConnection? connection = _byToken[token];
|
|
if (connection == null || !connection.handshakeComplete) {
|
|
continue;
|
|
}
|
|
await connection.sendInvocation('ReceiveQuestion', <Object?>[question]);
|
|
delivered++;
|
|
}
|
|
return delivered;
|
|
}
|
|
|
|
/// Parses inbound frames after the handshake.
|
|
void handleClientMessage(
|
|
QuestionsHubConnection connection,
|
|
String payload,
|
|
) {
|
|
for (final String message in TextMessageFormat.parse(payload)) {
|
|
final Map<String, dynamic> json =
|
|
jsonDecode(message) as Map<String, dynamic>;
|
|
final int? type = json['type'] as int?;
|
|
if (type == 6) {
|
|
unawaited(connection.sendRaw(SignalrProtocol.ping()));
|
|
}
|
|
}
|
|
}
|
|
}
|