// Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // Adapted from server/sdm.go in the NATS server Go source. namespace ZB.MOM.NatsNet.Server.Internal.DataStructures; /// /// Per-sequence data tracked by . /// Mirrors SDMBySeq in server/sdm.go. /// public readonly struct SdmBySeq { /// Whether this sequence was the last message for its subject. public bool Last { get; init; } /// Timestamp (nanoseconds UTC) when the removal/SDM was last proposed. public long Ts { get; init; } } /// /// Tracks pending subject delete markers (SDMs) and message removals for a stream. /// Used by JetStream cluster consensus to avoid redundant proposals. /// Mirrors SDMMeta in server/sdm.go. /// public sealed class StreamDeletionMeta { // Per-subject pending-count totals. private readonly Dictionary _totals = new(1); // Per-sequence data keyed by sequence number. private readonly Dictionary _pending = new(1); // ------------------------------------------------------------------------- // Header constants (forward-declared; populated in session 19 — JetStream). // isSubjectDeleteMarker checks these header keys. // ------------------------------------------------------------------------- // Mirrors JSMarkerReason header key (defined in jetstream.go). internal const string HeaderJsMarkerReason = "Nats-Marker-Reason"; // Mirrors KVOperation header key (defined in jetstream.go). internal const string HeaderKvOperation = "KV-Operation"; // Mirrors KVOperationValuePurge (defined in jetstream.go). internal const string KvOperationValuePurge = "PURGE"; /// /// Returns true when the given header block contains a subject delete marker /// (either a JetStream marker or a KV purge operation). /// Mirrors isSubjectDeleteMarker in server/sdm.go. /// public static bool IsSubjectDeleteMarker(ReadOnlySpan hdr) { // Simplified header scan: checks whether JSMarkerReason key is present // or whether KV-Operation equals "PURGE". // Full implementation depends on SliceHeader from session 08 (client.go). // Until then this provides the correct contract. var text = System.Text.Encoding.UTF8.GetString(hdr); if (text.Contains(HeaderJsMarkerReason)) return true; if (text.Contains($"{HeaderKvOperation}: {KvOperationValuePurge}")) return true; return false; } /// /// Tries to get the pending entry for . /// public bool TryGetPending(ulong seq, out SdmBySeq entry) => _pending.TryGetValue(seq, out entry); /// /// Sets the pending entry for . /// public void SetPending(ulong seq, SdmBySeq entry) => _pending[seq] = entry; /// /// Returns the pending count for , or 0 if not tracked. /// public ulong GetSubjectTotal(string subj) => _totals.TryGetValue(subj, out var cnt) ? cnt : 0; /// /// Clears all tracked data. /// Mirrors SDMMeta.empty. /// public void Empty() { _totals.Clear(); _pending.Clear(); } /// /// Tracks as pending and returns whether it was /// the last message for its subject. If the sequence is already tracked /// the existing Last value is returned without modification. /// Mirrors SDMMeta.trackPending. /// public bool TrackPending(ulong seq, string subj, bool last) { if (_pending.TryGetValue(seq, out var p)) return p.Last; _pending[seq] = new SdmBySeq { Last = last, Ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L }; _totals[subj] = _totals.TryGetValue(subj, out var cnt) ? cnt + 1 : 1; return last; } /// /// Removes and decrements the pending count for /// , deleting the subject entry when it reaches zero. /// Mirrors SDMMeta.removeSeqAndSubject. /// public void RemoveSeqAndSubject(ulong seq, string subj) { if (!_pending.Remove(seq)) return; if (_totals.TryGetValue(subj, out var msgs)) { if (msgs <= 1) _totals.Remove(subj); else _totals[subj] = msgs - 1; } } }