namespace NATS.Server.Auth; /// /// Tracks reply subjects that a client is temporarily allowed to publish to. /// Reference: Go client.go resp struct, setResponsePermissionIfNeeded. /// public sealed class ResponseTracker { private readonly int _maxMsgs; // 0 = unlimited private readonly TimeSpan _expires; // TimeSpan.Zero = no TTL private readonly Dictionary _replies = new(StringComparer.Ordinal); private readonly object _lock = new(); public ResponseTracker(int maxMsgs, TimeSpan expires) { _maxMsgs = maxMsgs; _expires = expires; } public int Count { get { lock (_lock) return _replies.Count; } } public void RegisterReply(string replySubject) { lock (_lock) { _replies[replySubject] = (DateTime.UtcNow, 0); } } public bool IsReplyAllowed(string subject) { lock (_lock) { if (!_replies.TryGetValue(subject, out var entry)) return false; if (_expires > TimeSpan.Zero && DateTime.UtcNow - entry.RegisteredAt > _expires) { _replies.Remove(subject); return false; } var newCount = entry.Count + 1; if (_maxMsgs > 0 && newCount > _maxMsgs) { _replies.Remove(subject); return false; } _replies[subject] = (entry.RegisteredAt, newCount); return true; } } public void Prune() { lock (_lock) { if (_expires <= TimeSpan.Zero && _maxMsgs <= 0) return; var now = DateTime.UtcNow; var toRemove = new List(); foreach (var (key, entry) in _replies) { if (_expires > TimeSpan.Zero && now - entry.RegisteredAt > _expires) toRemove.Add(key); else if (_maxMsgs > 0 && entry.Count >= _maxMsgs) toRemove.Add(key); } foreach (var key in toRemove) _replies.Remove(key); } } }