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 _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 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 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 processRetries({bool isConnected = true}) async { if (_pendingMessages.isEmpty) { return; } final messagesToRemove = []; 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); } } }