feat(transport): Improve socket transport mode selection and localization

This commit is contained in:
cogwheel0
2025-10-30 22:32:59 +05:30
parent f4561484f6
commit a00d64fc26
19 changed files with 441 additions and 50 deletions

View File

@@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart';
// import 'package:http_parser/http_parser.dart';
// Removed legacy websocket/socket.io imports
import 'package:uuid/uuid.dart';
import '../models/backend_config.dart';
import '../models/server_config.dart';
import '../models/user.dart';
import '../models/model.dart';
@@ -253,6 +254,44 @@ class ApiService {
return result;
}
Future<BackendConfig?> getBackendConfig() async {
try {
final response = await _dio.get('/api/config');
final data = response.data;
Map<String, dynamic>? jsonMap;
if (data is Map<String, dynamic>) {
jsonMap = data;
} else if (data is String && data.isNotEmpty) {
final decoded = json.decode(data);
if (decoded is Map<String, dynamic>) {
jsonMap = decoded;
}
}
if (jsonMap == null) {
return null;
}
return BackendConfig.fromJson(jsonMap);
} on DioException catch (e, stackTrace) {
_traceApi('Backend config request failed: $e');
DebugLogger.error(
'backend-config-error',
scope: 'api/config',
error: e,
stackTrace: stackTrace,
);
rethrow;
} catch (e, stackTrace) {
_traceApi('Backend config decode error: $e');
DebugLogger.error(
'backend-config-decode',
scope: 'api/config',
error: e,
stackTrace: stackTrace,
);
rethrow;
}
}
// Authentication
Future<Map<String, dynamic>> login(String username, String password) async {
try {

View File

@@ -26,7 +26,7 @@ class SettingsService {
static const String _voiceAutoSendKey = PreferenceKeys.voiceAutoSendFinal;
// Realtime transport preference
static const String _socketTransportModeKey =
PreferenceKeys.socketTransportMode; // 'auto' or 'ws'
PreferenceKeys.socketTransportMode; // 'polling' or 'ws'
// Quick pill visibility selections (max 2)
static const String _quickPillsKey = PreferenceKeys
.quickPills; // StringList of identifiers e.g. ['web','image','tools']
@@ -256,15 +256,27 @@ class SettingsService {
return _preferencesBox().put(_voiceAutoSendKey, value);
}
/// Transport mode: 'auto' (polling+websocket) or 'ws' (websocket only)
/// Transport mode: 'polling' (HTTP polling + WebSocket upgrade) or 'ws'
static Future<String> getSocketTransportMode() {
final value = _preferencesBox().get(_socketTransportModeKey) as String?;
return Future.value(value ?? 'ws');
final raw = _preferencesBox().get(_socketTransportModeKey) as String?;
if (raw == null) {
return Future.value('ws');
}
if (raw == 'auto') {
return Future.value('polling');
}
if (raw != 'polling' && raw != 'ws') {
return Future.value('ws');
}
return Future.value(raw);
}
static Future<void> setSocketTransportMode(String mode) {
if (mode != 'auto' && mode != 'ws') {
mode = 'auto';
if (mode == 'auto') {
mode = 'polling';
}
if (mode != 'polling' && mode != 'ws') {
mode = 'polling';
}
return _preferencesBox().put(_socketTransportModeKey, mode);
}
@@ -344,7 +356,7 @@ class AppSettings {
final String? voiceLocaleId;
final bool voiceHoldToTalk;
final bool voiceAutoSendFinal;
final String socketTransportMode; // 'auto' or 'ws'
final String socketTransportMode; // 'polling' or 'ws'
final List<String> quickPills; // e.g., ['web','image']
final bool sendOnEnter;
final String? ttsVoice;
@@ -566,8 +578,17 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
}
Future<void> setSocketTransportMode(String mode) async {
state = state.copyWith(socketTransportMode: mode);
await SettingsService.setSocketTransportMode(mode);
var sanitized = mode;
if (sanitized == 'auto') {
sanitized = 'polling';
}
if (sanitized != 'polling' && sanitized != 'ws') {
sanitized = 'polling';
}
if (state.socketTransportMode != sanitized) {
state = state.copyWith(socketTransportMode: sanitized);
}
await SettingsService.setSocketTransportMode(sanitized);
}
Future<void> setQuickPills(List<String> pills) async {

View File

@@ -22,6 +22,7 @@ typedef SocketChannelEventHandler =
class SocketService with WidgetsBindingObserver {
final ServerConfig serverConfig;
final bool websocketOnly;
final bool allowWebsocketUpgrade;
io.Socket? _socket;
String? _authToken;
bool _isAppForeground = true;
@@ -34,6 +35,7 @@ class SocketService with WidgetsBindingObserver {
required this.serverConfig,
String? authToken,
this.websocketOnly = false,
this.allowWebsocketUpgrade = true,
}) : _authToken = authToken {
final binding = WidgetsBinding.instance;
final lifecycle = binding.lifecycleState;
@@ -72,11 +74,18 @@ class SocketService with WidgetsBindingObserver {
} catch (_) {}
final path = '/ws/socket.io';
final usePollingOnly = !websocketOnly && !allowWebsocketUpgrade;
final transports = websocketOnly
? const ['websocket']
: usePollingOnly
? const ['polling']
: const ['polling', 'websocket'];
final builder = io.OptionBuilder()
// Transport selection - WebSocket only, no polling fallback
.setTransports(['websocket'])
.setRememberUpgrade(false)
.setUpgrade(false)
// Transport selection switches between WebSocket-only and polling fallback
.setTransports(transports)
.setRememberUpgrade(!websocketOnly && allowWebsocketUpgrade)
.setUpgrade(!websocketOnly && allowWebsocketUpgrade)
// Tune reconnect/backoff and timeouts
.setReconnectionAttempts(0) // 0/Infinity semantics: unlimited attempts
.setReconnectionDelay(1000)