Files
votianlt/app/lib/services/ack_tracker.dart

144 lines
4.0 KiB
Dart

import 'package:flutter/foundation.dart';
/// Represents a message awaiting acknowledgment
class PendingMessage {
/// Unique message identifier
final String messageId;
/// Target topic
final String topic;
/// The JSON payload to retry sending
final String jsonPayload;
/// When the message was originally sent
final DateTime sentAt;
/// Number of retry attempts so far
int retryCount;
PendingMessage({
required this.messageId,
required this.topic,
required this.jsonPayload,
required this.sentAt,
this.retryCount = 0,
});
}
/// Tracks pending messages awaiting acknowledgment and handles retries.
///
/// This class is extracted from WebSocketService for testability.
/// It manages:
/// - Tracking sent messages that require ACK
/// - Removing messages when ACK is received
/// - Retrying unacknowledged messages
/// - Timing out messages after max retries
class AckTracker {
final Map<String, PendingMessage> _pendingMessages = {};
/// Maximum number of retry attempts before timeout
final int maxRetries;
/// Callback to retry sending a message.
/// Returns true if send was successful.
final Future<bool> Function(String topic, String payload)? onRetry;
/// Callback when a message times out (max retries exceeded)
final void Function(String messageId, String topic)? onTimeout;
AckTracker({
this.maxRetries = 4,
this.onRetry,
this.onTimeout,
});
/// Track a sent message that requires acknowledgment
void track(String messageId, String topic, String payload) {
_pendingMessages[messageId] = PendingMessage(
messageId: messageId,
topic: topic,
jsonPayload: payload,
sentAt: DateTime.now(),
);
}
/// Remove a message from tracking (ACK received)
void acknowledge(String messageId) {
_pendingMessages.remove(messageId);
}
/// Check if a message is pending acknowledgment
bool isPending(String messageId) =>
_pendingMessages.containsKey(messageId);
/// Get the number of pending messages
int get pendingCount => _pendingMessages.length;
/// Get all pending message IDs
List<String> get pendingMessageIds =>
List.unmodifiable(_pendingMessages.keys);
/// Get a pending message by ID (for testing)
@visibleForTesting
PendingMessage? getPendingMessage(String messageId) =>
_pendingMessages[messageId];
/// Process all pending messages and retry if needed.
///
/// This should be called periodically (e.g., every 5 seconds).
/// Messages that exceed maxRetries will be timed out and removed.
///
/// Set [isConnected] to false to skip retry attempts while disconnected.
Future<void> processRetries({bool isConnected = true}) async {
if (_pendingMessages.isEmpty) {
return;
}
final messagesToRemove = <String>[];
for (final entry in _pendingMessages.entries) {
final messageId = entry.key;
final pending = entry.value;
if (pending.retryCount >= maxRetries) {
// Max retries exceeded - timeout
onTimeout?.call(messageId, pending.topic);
messagesToRemove.add(messageId);
} else if (isConnected) {
// Increment retry count and attempt resend
pending.retryCount++;
if (onRetry != null) {
await onRetry!(pending.topic, pending.jsonPayload);
}
}
}
// Remove timed out messages
for (final messageId in messagesToRemove) {
_pendingMessages.remove(messageId);
}
}
/// Clear all pending messages.
///
/// Primarily for testing purposes.
@visibleForTesting
void clearAll() => _pendingMessages.clear();
/// Clear pending messages for a specific topic pattern.
///
/// Useful for clearing login messages when auth response is received.
void clearForTopic(String topicPattern) {
final toRemove = _pendingMessages.entries
.where((e) => e.value.topic == topicPattern)
.map((e) => e.key)
.toList();
for (final messageId in toRemove) {
_pendingMessages.remove(messageId);
}
}
}