refactor: Projektstruktur in app/ und backend/ aufgeteilt
This commit is contained in:
143
app/lib/services/ack_tracker.dart
Normal file
143
app/lib/services/ack_tracker.dart
Normal file
@@ -0,0 +1,143 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user