connectSocket function
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;
}