// QoS 1 outgoing message tracker for MQTT with JetStream ack integration. // Go reference: golang/nats-server/server/mqtt.go // QoS 1 outbound tracking — mqttProcessPub (~line 1200) // trackPublish — maps packet IDs to stream sequences for ack tracking. using System.Collections.Concurrent; namespace NATS.Server.Mqtt; /// /// Tracks outgoing QoS 1 messages pending PUBACK from the client. /// Maps packet IDs to JetStream stream sequences for ack-based cleanup. /// Go reference: server/mqtt.go — mqttProcessPub, trackPublish. /// public sealed class MqttQoS1Tracker { private readonly ConcurrentDictionary _pending = new(); private ushort _nextPacketId; private readonly Lock _lock = new(); /// Number of messages pending PUBACK. public int PendingCount => _pending.Count; /// /// Registers an outgoing QoS 1 message and assigns a packet ID. /// Returns the assigned packet ID. /// /// MQTT topic for the outbound message. /// Outbound payload bytes. /// Optional JetStream stream sequence tied to this delivery. public ushort Register(string topic, byte[] payload, ulong streamSequence = 0) { var id = GetNextPacketId(); _pending[id] = new QoS1PendingMessage { PacketId = id, Topic = topic, Payload = payload, SentAtUtc = DateTime.UtcNow, DeliveryCount = 1, StreamSequence = streamSequence, }; return id; } /// /// Acknowledges receipt of a PUBACK for the given packet ID. /// Returns the pending message if found, or null. /// /// MQTT packet identifier from PUBACK. public QoS1PendingMessage? Acknowledge(ushort packetId) { return _pending.TryRemove(packetId, out var msg) ? msg : null; } /// /// Returns all pending messages for redelivery (e.g., on reconnect). /// Increments their delivery count. /// public IReadOnlyList GetPendingForRedelivery() { var result = new List(); foreach (var kvp in _pending) { var msg = kvp.Value; msg.DeliveryCount++; msg.SentAtUtc = DateTime.UtcNow; result.Add(msg); } return result; } /// /// Checks if a packet ID is pending acknowledgment. /// /// MQTT packet identifier to check. public bool IsPending(ushort packetId) => _pending.ContainsKey(packetId); /// Clears all pending messages. public void Clear() => _pending.Clear(); private ushort GetNextPacketId() { lock (_lock) { _nextPacketId++; if (_nextPacketId == 0) _nextPacketId = 1; // MQTT spec: packet ID must be non-zero return _nextPacketId; } } } /// /// A QoS 1 message pending PUBACK from the client. /// public sealed class QoS1PendingMessage { /// MQTT packet identifier assigned to this outbound QoS1 message. public ushort PacketId { get; init; } /// MQTT topic associated with the message. public string Topic { get; init; } = string.Empty; /// Outbound payload bytes awaiting PUBACK. public byte[] Payload { get; init; } = []; /// UTC timestamp when this message was last sent. public DateTime SentAtUtc { get; set; } /// Number of delivery attempts for this packet. public int DeliveryCount { get; set; } = 1; /// /// JetStream stream sequence for this message. 0 if not backed by JetStream. /// Used to ack the message in the stream on PUBACK. /// Go reference: server/mqtt.go trackPublish — maps packet ID → stream sequence. /// public ulong StreamSequence { get; init; } }