TF-3334 Use queue to handle multiple refresh changes mailbox from incoming websocket

This commit is contained in:
dab246
2024-12-23 12:24:50 +07:00
committed by Dat H. Pham
parent 6063f4b832
commit dfd622d7ec
19 changed files with 707 additions and 154 deletions
@@ -5,6 +5,7 @@ import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart';
import 'package:debounce_throttle/debounce_throttle.dart';
import 'package:fcm/model/type_name.dart';
import 'package:flutter/material.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
@@ -18,6 +19,7 @@ import 'package:tmail_ui_user/features/push_notification/presentation/controller
import 'package:tmail_ui_user/features/push_notification/presentation/extensions/state_change_extension.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/listener/email_change_listener.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/listener/mailbox_change_listener.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/utils/fcm_utils.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
import 'package:tmail_ui_user/main/utils/app_config.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
@@ -36,6 +38,7 @@ class WebSocketController extends PushBaseController {
Timer? _webSocketPingTimer;
StreamSubscription? _webSocketSubscription;
AppLifecycleListener? _appLifecycleListener;
Debouncer<StateChange?>? _stateChangeDebouncer;
@override
void handleFailureViewState(Failure failure) {
@@ -105,8 +108,9 @@ class WebSocketController extends PushBaseController {
_webSocketSubscription?.cancel();
_webSocketChannel = null;
_webSocketPingTimer?.cancel();
_stateChangeDebouncer?.cancel();
}
void _handleWebSocketConnectionSuccess(WebSocketConnectionSuccess success) {
log('WebSocketController::_handleWebSocketConnectionSuccess(): $success');
_cleanUpWebSocketResources();
@@ -117,6 +121,7 @@ class WebSocketController extends PushBaseController {
_pingWebSocket();
}
_listenToWebSocket();
_initStateChangeDeouncerTimer();
}
void _handleWebSocketConnectionRetry() {
@@ -150,14 +155,7 @@ class WebSocketController extends PushBaseController {
try {
final stateChange = StateChange.fromJson(data);
final mapTypeState = stateChange.getMapTypeState(accountId!);
mappingTypeStateToAction(
mapTypeState,
accountId!,
emailChangeListener: EmailChangeListener.instance,
mailboxChangeListener: MailboxChangeListener.instance,
session!.username,
session: session);
_stateChangeDebouncer?.value = stateChange;
} catch (e) {
logError('WebSocketController::_listenToWebSocket(): Data is not StateChange');
}
@@ -173,4 +171,31 @@ class WebSocketController extends PushBaseController {
},
);
}
void _initStateChangeDeouncerTimer() {
_stateChangeDebouncer = Debouncer<StateChange?>(
const Duration(milliseconds: FcmUtils.durationMessageComing),
initialValue: null,
);
_stateChangeDebouncer?.values.listen(_handleStateChange);
}
void _handleStateChange(StateChange? stateChange) {
try {
if (stateChange == null || accountId == null || session == null) return;
final mapTypeState = stateChange.getMapTypeState(accountId!);
mappingTypeStateToAction(
mapTypeState,
accountId!,
emailChangeListener: EmailChangeListener.instance,
mailboxChangeListener: MailboxChangeListener.instance,
session!.username,
session: session,
);
} catch (e) {
logError('WebSocketController::_handleStateChange:Exception = $e');
}
}
}
@@ -0,0 +1,13 @@
import 'package:equatable/equatable.dart';
import 'package:jmap_dart_client/jmap/core/state.dart' as jmap;
class WebSocketMessage with EquatableMixin {
final jmap.State newState;
WebSocketMessage({required this.newState});
String get id => newState.value;
@override
List<Object?> get props => [newState];
}
@@ -0,0 +1,121 @@
import 'dart:async';
import 'dart:collection';
import 'package:core/utils/app_logger.dart';
import 'package:flutter/cupertino.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/websocket/web_socket_message.dart';
typedef ProcessMessageCallback = Future<void> Function(WebSocketMessage message);
typedef OnErrorCallback = void Function(dynamic error, StackTrace stackTrace);
class WebSocketQueueHandler {
static const int _maxQueueSize = 128;
static const int _maxProcessedIdsSize = 128;
final Queue<WebSocketMessage> _messageQueue = Queue<WebSocketMessage>();
final Queue<String> _processedMessageIds = Queue<String>();
Completer<void>? _processingLock;
final _queueController = StreamController<WebSocketMessage>.broadcast();
final ProcessMessageCallback processMessageCallback;
final OnErrorCallback? onErrorCallback;
WebSocketQueueHandler({
required this.processMessageCallback,
this.onErrorCallback,
}) {
_queueController.stream.listen((_) {
_processQueue();
});
}
void enqueue(WebSocketMessage message) {
if (isMessageProcessed(message.id)) {
log('WebSocketQueueHandler::enqueue:Message ${message.id} already processed, skipping');
return;
}
if (queueSize >= _maxQueueSize) {
log('WebSocketQueueHandler::enqueue:Queue full, removing oldest message');
_messageQueue.removeFirst();
}
_messageQueue.add(message);
_queueController.add(message);
}
Future<void> _processQueue() async {
if (_processingLock != null) {
return;
}
_processingLock = Completer<void>();
try {
while (queueSize > 0) {
final message = _messageQueue.removeFirst();
try {
await processMessageCallback(message);
} catch (e, stackTrace) {
logError('WebSocketQueueHandler::_processQueue:Error processing message ${message.id}: $e');
onErrorCallback?.call(e, stackTrace);
} finally {
_addToProcessedMessages(message.id);
}
}
} finally {
_processingLock?.complete();
_processingLock = null;
if (queueSize > 0) {
scheduleMicrotask(() => _queueController.add(_messageQueue.first));
}
}
}
void _addToProcessedMessages(String messageId) {
if (_processedMessageIds.length >= _maxProcessedIdsSize) {
_processedMessageIds.removeFirst();
}
_processedMessageIds.add(messageId);
}
void removeMessagesUpToCurrent(String messageId) {
final isCurrentStateExist = _messageQueue
.any((message) => message.id == messageId);
if (!isCurrentStateExist) {
log('WebSocketQueueHandler::removeMessagesUpToCurrent:Current state $messageId not found in the queue.');
return;
}
while (queueSize > 0) {
final removedMessage = _messageQueue.removeFirst();
if (removedMessage.id == messageId) {
break;
}
}
log('WebSocketQueueHandler::removeMessagesUpToCurrent:Updated Queue: $queueSize');
}
@visibleForTesting
Future<void> waitForEmpty() async {
while (_messageQueue.isNotEmpty || _processingLock != null) {
if (_processingLock != null) {
await _processingLock!.future;
}
await Future.delayed(const Duration(milliseconds: 100));
}
}
int get queueSize => _messageQueue.length;
bool isMessageProcessed(String messageId) => _processedMessageIds.contains(messageId);
void dispose() {
_queueController.close();
}
}