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. A failed connection throws a MediaSocketConnectException: its phase says where it stopped (before the transport opened, after, rejected by the server, timeout), reason/stage carry the server's connection-rejected verdict, elapsedMs/online help tell a network problem from a refusal, retryable is false when the same credentials cannot succeed, and userMessage is ready for an alert.

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;
  final startedAt = DateTime.now();
  var transportOpened = false;
  String? rejectionReason;
  String? rejectionStage;

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

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

  String? describeTransportError(dynamic error) {
    if (error == null) return null;
    final text = error is Map ? (error['message'] ?? error).toString() : error.toString();
    return text.length > 200 ? text.substring(0, 200) : text;
  }

  void finishWithError(MediaSocketConnectPhase phase, [dynamic error]) {
    if (settled) return;
    settled = true;
    cleanupAdmissionListeners();
    socket.disconnect();
    completer.completeError(MediaSocketConnectException(
      phase: rejectionReason != null ? MediaSocketConnectPhase.rejected : phase,
      reason: rejectionReason,
      stage: rejectionStage,
      elapsedMs: DateTime.now().difference(startedAt).inMilliseconds,
      online: currentOnlineState(),
      transportError: describeTransportError(error),
      link: options.link,
    ));
  }

  // 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 = (error) => finishWithError(
      transportOpened
          ? MediaSocketConnectPhase.afterOpen
          : MediaSocketConnectPhase.beforeOpen,
      error);
  handleSocketError = (error) => finishWithError(
      transportOpened
          ? MediaSocketConnectPhase.afterOpen
          : MediaSocketConnectPhase.beforeOpen,
      error);
  handleDisconnectBeforeReady =
      (reason) => finishWithError(MediaSocketConnectPhase.afterOpen, reason);
  handleTransportOpen = (_) => transportOpened = true;
  // Media admission v2: the node says why before it closes the handshake. A refusal is final for these credentials,
  // so it is reported as such and never retried as a network error.
  handleConnectionRejected = (payload) {
    final data = payload is Map ? payload : const {};
    rejectionReason = normalizeRejectionReason(data['reason']);
    rejectionStage = data['stage'] is String ? data['stage'] as String : null;
    finishWithError(MediaSocketConnectPhase.rejected);
  };

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

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

  return completer.future;
}