TF-3334 Use queue to handle multiple refresh changes mailbox from incoming websocket
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user