fartstack/backend/lib/socket_manager.dart

139 lines
4.6 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 _gameRoomSpSubs = <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 (!_gameRoomSpSubs.containsKey(roomUuid)) {
final status = await spawnGameRoomIsolate(roomUuid);
if (status == Status.failure) {
return status;
}
}
_connections.putIfAbsent(roomUuid, () => <String, WebSocketChannel>{});
_connections[roomUuid]![userUuid] = connection;
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 (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) {
// 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 RoomPingMessage():
if (message.dest != PingDestination.client) {
_logger.warning('Got room ping meant for server');
return;
}
broadcastToRoom(
message.roomUuid,
message,
);
case PlayerVoteMessage():
// TODO: Handle this case.
throw UnimplementedError();
case PingMessage():
// TODO: Handle this case.
throw UnimplementedError();
}
} else {
_logger.info('Unknown message: $message');
}
});
_gameRoomSpSubs[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 = _gameRoomSpSubs.remove(roomUuid);
await sub?.cancel();
}
return Status.failure;
}
}
enum Status { success, failure }