connectSocket function

Future<Socket> connectSocket(
  1. ConnectSocketOptions options
)

Connects to a media socket with the specified options. Validates the API key or token and initiates a socket connection.

Throws an exception if inputs are invalid or if connection fails.

Example usage:

final options = ConnectSocketOptions(
  apiUserName: "user123",
  apiKey: "yourApiKeyHere",
  link: "https://socketlink.com",
);

try {
  final socket = await connectSocket(options);
  print("Connected to socket with ID: ${socket.id}");
} catch (error) {
  print("Failed to connect to socket: $error");
}

Implementation

Future<io.Socket> connectSocket(ConnectSocketOptions options) async {
  // Input validation
  if (options.apiUserName.isEmpty) throw Exception('API username required.');
  if ((options.apiKey?.isEmpty ?? true) &&
      (options.apiToken?.isEmpty ?? true)) {
    throw Exception('API key or token required.');
  }
  if (options.link.isEmpty) throw Exception('Socket link required.');

  // Validate API key or token format
  bool useKey = false;
  try {
    if (options.apiKey?.length == 64 &&
        await validateApiKeyToken(options.apiKey!)) {
      useKey = true;
    } else if (options.apiToken?.length == 64 &&
        await validateApiKeyToken(options.apiToken!)) {
      useKey = false;
    } else {
      throw Exception('Invalid API key or token format.');
    }
  } catch (error) {
    throw Exception('Invalid API key or token.');
  }

  // Configure socket options based on whether apiKey or apiToken is used
  final query = useKey
      ? {'apiUserName': options.apiUserName, 'apiKey': options.apiKey}
      : {'apiUserName': options.apiUserName, 'apiToken': options.apiToken};

  final socket = io.io('${options.link}/media', {
    'transports': ['websocket'],
    'query': query,
    'autoConnect': false,
  });

  final completer = Completer<io.Socket>();
  Timer? admissionTimer;
  var settled = false;

  late void Function(dynamic) handleConnectionSuccess;
  late void Function(dynamic) handleConnectError;
  late void Function(dynamic) handleSocketError;
  late void Function(dynamic) handleDisconnectBeforeReady;

  void cleanupAdmissionListeners() {
    admissionTimer?.cancel();
    socket.off('connection-success', handleConnectionSuccess);
    socket.off('connect_error', handleConnectError);
    socket.off('error', handleSocketError);
    socket.off('disconnect', handleDisconnectBeforeReady);
  }

  void finishWithError(String stage) {
    if (settled) return;
    settled = true;
    cleanupAdmissionListeners();
    socket.disconnect();
    completer.completeError(
      Exception('Media socket admission failed at $stage.'),
    );
  }

  // A raw Socket.IO connection is only transport establishment. The media
  // server emits connection-success after its credential admission finishes;
  // do not expose the socket to callers before that application-level proof.
  handleConnectionSuccess = (_) {
    if (settled) return;
    settled = true;
    cleanupAdmissionListeners();
    completer.complete(socket);
  };

  // Handle connection error
  handleConnectError = (_) => finishWithError('transport_connect_error');
  handleSocketError =
      (_) => finishWithError('socket_error_before_authorization');
  handleDisconnectBeforeReady =
      (_) => finishWithError('disconnected_before_authorization');

  socket.on('connection-success', handleConnectionSuccess);
  socket.on('connect_error', handleConnectError);
  socket.on('error', handleSocketError);
  socket.on('disconnect', handleDisconnectBeforeReady);

  admissionTimer = Timer(
    const Duration(seconds: 12),
    () => finishWithError('application_authorization_timeout'),
  );
  socket.connect();

  return completer.future;
}