MIF_E31230910_MP-HRIS-MOBILE/lib/services/websocket_service.dart

163 lines
4.2 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../core/cache_manager.dart';
import '../core/constants/api_url.dart';
class WebSocketService extends ChangeNotifier {
static final WebSocketService _instance = WebSocketService._internal();
factory WebSocketService() => _instance;
WebSocketService._internal();
WebSocketChannel? _channel;
Timer? _reconnectTimer;
bool _isConnected = false;
bool _disposed = false;
final _eventController = StreamController<Map<String, dynamic>>.broadcast();
Stream<Map<String, dynamic>> get eventStream => _eventController.stream;
bool get isConnected => _isConnected;
Future<void> connect() async {
if (_isConnected || _disposed) return;
try {
final baseUrl = ApiUrl.baseUrl;
final token = CacheManager.authBox.get('auth_token') ?? '';
final wsUrl = _buildWsUrl(baseUrl);
if (wsUrl == null) return;
final uri = Uri.parse('$wsUrl/app/mpg-hris-key');
_channel = WebSocketChannel.connect(uri);
await _channel!.ready;
_isConnected = true;
_reconnectTimer?.cancel();
notifyListeners();
debugPrint('[WS] Terhubung ke $uri');
_channel!.stream.listen(
(message) => _handleMessage(message, token),
onDone: () {
debugPrint('[WS] Koneksi terputus');
_isConnected = false;
notifyListeners();
_scheduleReconnect();
},
onError: (error) {
debugPrint('[WS] Error: $error');
_isConnected = false;
notifyListeners();
_scheduleReconnect();
},
);
_subscribeToChannels(token);
} catch (e) {
debugPrint('[WS] Gagal connect: $e');
_isConnected = false;
_scheduleReconnect();
}
}
String? _buildWsUrl(String apiBaseUrl) {
try {
final uri = Uri.parse(apiBaseUrl);
final scheme = uri.scheme == 'https' ? 'wss' : 'ws';
if (uri.host.contains('trycloudflare.com')) {
return '$scheme://${uri.host}';
}
return '$scheme://${uri.host}:8080';
} catch (e) {
debugPrint('[WS] URL invalid: $e');
return null;
}
}
void _subscribeToChannels(String token) {
final userId = CacheManager.authBox.get('user_id');
if (userId == null) return;
_sendSubscribe('private-user.$userId', token);
_sendSubscribe('pengumuman', null);
}
void _sendSubscribe(String channel, String? token) {
final data = {
'event': 'pusher:subscribe',
'data': {
'channel': channel,
if (token != null) 'auth': token,
},
};
_channel?.sink.add(jsonEncode(data));
debugPrint('[WS] Subscribe: $channel');
}
void _handleMessage(dynamic rawMessage, String token) {
try {
final message = jsonDecode(rawMessage as String) as Map<String, dynamic>;
final event = message['event'] as String?;
if (event == 'pusher:connection_established') {
debugPrint('[WS] Connection established');
_subscribeToChannels(token);
return;
}
if (event == 'pusher_internal:subscription_succeeded') {
debugPrint('[WS] Subscription berhasil: ${message['channel']}');
return;
}
if (event != null && !event.startsWith('pusher')) {
final dataRaw = message['data'];
final data = dataRaw is String ? jsonDecode(dataRaw) : dataRaw;
_eventController.add({
'event': event,
'channel': message['channel'],
'data': data,
});
debugPrint('[WS] Event diterima: $event');
}
} catch (e) {
debugPrint('[WS] Parse error: $e');
}
}
void _scheduleReconnect() {
_reconnectTimer?.cancel();
if (_disposed) return;
_reconnectTimer = Timer(const Duration(seconds: 5), () {
debugPrint('[WS] Reconnecting...');
connect();
});
}
void disconnect() {
_reconnectTimer?.cancel();
_channel?.sink.close();
_channel = null;
_isConnected = false;
notifyListeners();
debugPrint('[WS] Disconnected');
}
@override
void dispose() {
_disposed = true;
disconnect();
_eventController.close();
super.dispose();
}
}