144 lines
4.0 KiB
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);
|
|
}
|
|
}
|
|
}
|