import 'package:flutter/foundation.dart'; import '../models/message_envelope.dart'; /// Result of unwrapping a message envelope class UnwrapResult { /// The unwrapped payload final dynamic payload; /// The message ID (null if not an envelope) final String? messageId; /// Whether this message requires acknowledgment final bool requiresAck; UnwrapResult({ required this.payload, this.messageId, this.requiresAck = false, }); } /// Handles message envelope unwrapping and deduplication. /// /// This class is extracted from WebSocketService for testability. /// It manages: /// - Detecting and unwrapping MessageEnvelope structures /// - Deduplicating messages by messageId /// - Triggering ACK callbacks when required class MessageHandler { final Set _processedMessageIds = {}; /// Maximum number of message IDs to track for deduplication final int maxProcessedIds; /// Callback invoked when an ACK should be sent final void Function(String messageId)? onAckRequired; MessageHandler({ this.maxProcessedIds = 100, this.onAckRequired, }); /// Check if data is a valid MessageEnvelope structure. /// /// A valid envelope must contain: /// - messageId /// - timestamp /// - topic /// - payload bool isEnvelopeMessage(dynamic data) { if (data is! Map) return false; return data.containsKey('messageId') && data.containsKey('timestamp') && data.containsKey('topic') && data.containsKey('payload'); } /// Unwrap a message envelope and handle deduplication. /// /// Returns null if the message was already processed (duplicate). /// For duplicates, still triggers onAckRequired if the original required ACK. /// /// Returns [UnwrapResult] with payload and ACK info for new messages. /// If data is not an envelope, returns it as-is with requiresAck=false. UnwrapResult? unwrapEnvelope(dynamic data) { if (!isEnvelopeMessage(data)) { // Not an envelope, return data as-is (no ACK needed) return UnwrapResult( payload: data, messageId: null, requiresAck: false, ); } final envelope = MessageEnvelope.fromJson(data as Map); // Check for duplicate if (_processedMessageIds.contains(envelope.messageId)) { // Still send ACK for duplicate messages if (envelope.requiresAck && onAckRequired != null) { onAckRequired!(envelope.messageId); } return null; } // Track this message as processed _processedMessageIds.add(envelope.messageId); // Limit set size to prevent memory growth (FIFO eviction) if (_processedMessageIds.length > maxProcessedIds) { _processedMessageIds.remove(_processedMessageIds.first); } return UnwrapResult( payload: envelope.payload, messageId: envelope.messageId, requiresAck: envelope.requiresAck, ); } /// Check if a message ID was already processed bool wasProcessed(String messageId) => _processedMessageIds.contains(messageId); /// Get the count of tracked message IDs int get processedCount => _processedMessageIds.length; /// Clear all processed message IDs. /// /// Primarily for testing purposes. @visibleForTesting void clearProcessedIds() => _processedMessageIds.clear(); }