// 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;
}
}
}