fartstack/backend/lib/socket_manager.dart

150 lines
4.9 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:backend/game_room_manager.dart';
import 'package:dart_frog_web_socket/dart_frog_web_socket.dart';
import 'package:logging/logging.dart';
import 'package:shared_models/room.dart';
final _logger = Logger('SocketManager');
class SocketManager {
// Default constructor returns instance
factory SocketManager() {
return instance;
}
// Private constructor
SocketManager._();
// Singleton instance
static final SocketManager instance = SocketManager._();
// Store connections: GameRoomUuid -> UserUuid -> WebSocketChannel
final _connections = <String, Map<String, WebSocketChannel>>{};
// Store isolate port and stream subscription to said port
final _gameRoomSendPorts = <String, SendPort>{};
final _gameRoomPortSubs = <String, StreamSubscription<dynamic>>{};
final _gameRoomUserWsSubs = <String, StreamSubscription<dynamic>>{};
// Add a new connection
Future<Status> addConnection(
WebSocketChannel connection, {
required String roomUuid,
required String userUuid,
}) async {
_logger.finer('Adding connection to socket manager for user $userUuid in room $roomUuid');
if (!_gameRoomPortSubs.containsKey(roomUuid)) {
final status = await spawnGameRoomIsolate(roomUuid);
if (status == Status.failure) {
return status;
}
}
_connections.putIfAbsent(roomUuid, () => <String, WebSocketChannel>{});
_connections[roomUuid]![userUuid] = connection;
_logger.fine('Listening to websocket for messages');
// ignore: cancel_subscriptions
final sub = connection.stream.listen(_gameRoomMessageListener);
_gameRoomUserWsSubs[userUuid] = sub;
return Status.success;
}
// Remove a connection
void removeConnection(String roomUuid, String userUuid) {
_connections[roomUuid]?.remove(userUuid);
if (_connections[roomUuid]?.isEmpty ?? false) {
_connections.remove(roomUuid);
}
}
// Get a specific user's connection
WebSocketChannel? getConnection(String roomUuid, String userUuid) {
return _connections[roomUuid]?[userUuid];
}
// Get all connections in a room
Map<String, WebSocketChannel>? getRoomConnections(String roomUuid) {
return _connections[roomUuid];
}
// Broadcast message to all users in a room except sender
void broadcastToRoom(String roomUuid, GameRoomMessage message, {String? excludeUserUuid}) {
_logger.fine('Broadcasting ${message.type} to room $roomUuid');
_connections[roomUuid]?.forEach((userUuid, connection) {
if (message is PingMessage) {
message.userUuid = userUuid;
}
if (userUuid != excludeUserUuid) {
connection.sink.add(jsonEncode(message.toJson()));
}
});
}
// Check if a user is connected
bool isConnected(String roomUuid, String userUuid) {
return _connections[roomUuid]?.containsKey(userUuid) ?? false;
}
Future<Status> spawnGameRoomIsolate(String roomUuid) async {
try {
_logger.finest('Spawning isolate for room $roomUuid');
if (_gameRoomSendPorts.containsKey(roomUuid)) {
_logger.severe('Tried to create a sendPort for an existing room uuid: $roomUuid', null, StackTrace.current);
throw Exception('Cannot create sendPort, room already has one');
}
final receivePort = ReceivePort();
final sp = receivePort.sendPort;
await Isolate.spawn(LiveGameRoom.spawn, LiveGameRoomData(roomUuid: roomUuid, wsSendPort: sp));
// used to get sendport
final completer = Completer<SendPort>();
// ignore: cancel_subscriptions
final sub = receivePort.listen((message) => _gameRoomPortListener(message, completer));
_gameRoomPortSubs[roomUuid] = sub;
_gameRoomSendPorts[roomUuid] = await completer.future;
_logger.info('Spawned new game room $roomUuid, listening to messages from room.');
return Status.success;
} catch (e) {
_logger.severe('Failed to spawn game room isolate', e, StackTrace.current);
_gameRoomSendPorts.remove(roomUuid);
final sub = _gameRoomPortSubs.remove(roomUuid);
await sub?.cancel();
}
return Status.failure;
}
void _gameRoomPortListener(dynamic message, Completer<SendPort> completer) {
// first message from new isolate will be its SendPort
if (!completer.isCompleted) {
completer.complete(message as SendPort);
return;
}
if (message is GameRoomMessage) {
switch (message) {
case PlayerVoteMessage():
// TODO: Handle this case.
throw UnimplementedError();
case PingMessage():
if (message.dest != PingDestination.client) {
_logger.warning('Got room ping meant for server');
return;
}
broadcastToRoom(
message.roomUuid,
message,
);
}
} else {
_logger.info('Unknown message: $message');
}
}
void _gameRoomMessageListener(dynamic message) {}
}
enum Status { success, failure }