using System.Text; using System.Text.Json; using NATS.Server.JetStream.Models; namespace NATS.Server.JetStream.Api.Handlers; public static class ConsumerApiHandlers { private const string CreatePrefix = JetStreamApiSubjects.ConsumerCreate; private const string InfoPrefix = JetStreamApiSubjects.ConsumerInfo; private const string NamesPrefix = JetStreamApiSubjects.ConsumerNames; private const string ListPrefix = JetStreamApiSubjects.ConsumerList; private const string DeletePrefix = JetStreamApiSubjects.ConsumerDelete; private const string PausePrefix = JetStreamApiSubjects.ConsumerPause; private const string ResetPrefix = JetStreamApiSubjects.ConsumerReset; private const string UnpinPrefix = JetStreamApiSubjects.ConsumerUnpin; private const string NextPrefix = JetStreamApiSubjects.ConsumerNext; public static JetStreamApiResponse HandleCreate(string subject, ReadOnlySpan payload, ConsumerManager consumerManager) { var parsed = ParseSubject(subject, CreatePrefix); if (parsed == null) return JetStreamApiResponse.NotFound(subject); var (stream, durableName) = parsed.Value; var config = ParseConfig(payload); if (string.IsNullOrWhiteSpace(config.DurableName)) config.DurableName = durableName; return consumerManager.CreateOrUpdate(stream, config); } public static JetStreamApiResponse HandleInfo(string subject, ConsumerManager consumerManager) { var parsed = ParseSubject(subject, InfoPrefix); if (parsed == null) return JetStreamApiResponse.NotFound(subject); var (stream, durableName) = parsed.Value; return consumerManager.GetInfo(stream, durableName); } public static JetStreamApiResponse HandleDelete(string subject, ConsumerManager consumerManager) { var parsed = ParseSubject(subject, DeletePrefix); if (parsed == null) return JetStreamApiResponse.NotFound(subject); var (stream, durableName) = parsed.Value; return consumerManager.Delete(stream, durableName) ? JetStreamApiResponse.SuccessResponse() : JetStreamApiResponse.NotFound(subject); } public static JetStreamApiResponse HandleNames(string subject, ConsumerManager consumerManager) { var stream = ParseStreamSubject(subject, NamesPrefix); if (stream == null) return JetStreamApiResponse.NotFound(subject); return new JetStreamApiResponse { ConsumerNames = consumerManager.ListNames(stream), }; } public static JetStreamApiResponse HandleList(string subject, ConsumerManager consumerManager) { var stream = ParseStreamSubject(subject, ListPrefix); if (stream == null) return JetStreamApiResponse.NotFound(subject); return new JetStreamApiResponse { ConsumerNames = consumerManager.ListNames(stream), }; } public static JetStreamApiResponse HandlePause(string subject, ReadOnlySpan payload, ConsumerManager consumerManager) { var parsed = ParseSubject(subject, PausePrefix); if (parsed == null) return JetStreamApiResponse.NotFound(subject); var (stream, durableName) = parsed.Value; var paused = ParsePause(payload); return consumerManager.Pause(stream, durableName, paused) ? JetStreamApiResponse.SuccessResponse() : JetStreamApiResponse.NotFound(subject); } public static JetStreamApiResponse HandleReset(string subject, ConsumerManager consumerManager) { var parsed = ParseSubject(subject, ResetPrefix); if (parsed == null) return JetStreamApiResponse.NotFound(subject); var (stream, durableName) = parsed.Value; return consumerManager.Reset(stream, durableName) ? JetStreamApiResponse.SuccessResponse() : JetStreamApiResponse.NotFound(subject); } public static JetStreamApiResponse HandleUnpin(string subject, ConsumerManager consumerManager) { var parsed = ParseSubject(subject, UnpinPrefix); if (parsed == null) return JetStreamApiResponse.NotFound(subject); var (stream, durableName) = parsed.Value; return consumerManager.Unpin(stream, durableName) ? JetStreamApiResponse.SuccessResponse() : JetStreamApiResponse.NotFound(subject); } public static JetStreamApiResponse HandleNext(string subject, ReadOnlySpan payload, ConsumerManager consumerManager, StreamManager streamManager) { var parsed = ParseSubject(subject, NextPrefix); if (parsed == null) return JetStreamApiResponse.NotFound(subject); var (stream, durableName) = parsed.Value; var batch = ParseBatch(payload); var pullBatch = consumerManager.FetchAsync(stream, durableName, batch, streamManager, default).GetAwaiter().GetResult(); return new JetStreamApiResponse { PullBatch = new JetStreamPullBatch { Messages = pullBatch.Messages .Select(m => new JetStreamDirectMessage { Sequence = m.Sequence, Subject = m.Subject, Payload = Encoding.UTF8.GetString(m.Payload.Span), }) .ToArray(), }, }; } private static (string Stream, string Durable)? ParseSubject(string subject, string prefix) { if (!subject.StartsWith(prefix, StringComparison.Ordinal)) return null; var remainder = subject[prefix.Length..]; var split = remainder.Split('.', 2, StringSplitOptions.RemoveEmptyEntries); if (split.Length != 2) return null; return (split[0], split[1]); } private static ConsumerConfig ParseConfig(ReadOnlySpan payload) { if (payload.IsEmpty) return new ConsumerConfig(); try { using var doc = JsonDocument.Parse(payload.ToArray()); var root = doc.RootElement; var config = new ConsumerConfig(); if (root.TryGetProperty("durable_name", out var durableEl)) config.DurableName = durableEl.GetString() ?? string.Empty; if (root.TryGetProperty("filter_subject", out var filterEl)) config.FilterSubject = filterEl.GetString(); if (root.TryGetProperty("filter_subjects", out var filterSubjectsEl) && filterSubjectsEl.ValueKind == JsonValueKind.Array) { foreach (var item in filterSubjectsEl.EnumerateArray()) { var filter = item.GetString(); if (!string.IsNullOrWhiteSpace(filter)) config.FilterSubjects.Add(filter); } } if (root.TryGetProperty("ephemeral", out var ephemeralEl) && ephemeralEl.ValueKind == JsonValueKind.True) config.Ephemeral = true; if (root.TryGetProperty("push", out var pushEl) && pushEl.ValueKind == JsonValueKind.True) config.Push = true; if (root.TryGetProperty("heartbeat_ms", out var hbEl) && hbEl.TryGetInt32(out var hbMs)) config.HeartbeatMs = hbMs; if (root.TryGetProperty("ack_wait_ms", out var ackWaitEl) && ackWaitEl.TryGetInt32(out var ackWait)) config.AckWaitMs = ackWait; if (root.TryGetProperty("max_deliver", out var maxDeliverEl) && maxDeliverEl.TryGetInt32(out var maxDeliver)) config.MaxDeliver = Math.Max(maxDeliver, 0); if (root.TryGetProperty("max_ack_pending", out var maxAckPendingEl) && maxAckPendingEl.TryGetInt32(out var maxAckPending)) config.MaxAckPending = Math.Max(maxAckPending, 0); if (root.TryGetProperty("flow_control", out var flowControlEl) && flowControlEl.ValueKind is JsonValueKind.True or JsonValueKind.False) config.FlowControl = flowControlEl.GetBoolean(); if (root.TryGetProperty("rate_limit_bps", out var rateLimitEl) && rateLimitEl.TryGetInt64(out var rateLimit)) config.RateLimitBps = Math.Max(rateLimit, 0); if (root.TryGetProperty("opt_start_seq", out var optStartSeqEl) && optStartSeqEl.TryGetUInt64(out var optStartSeq)) config.OptStartSeq = optStartSeq; if (root.TryGetProperty("opt_start_time_utc", out var optStartTimeEl) && optStartTimeEl.ValueKind == JsonValueKind.String && DateTime.TryParse(optStartTimeEl.GetString(), out var optStartTime)) { config.OptStartTimeUtc = optStartTime.ToUniversalTime(); } if (root.TryGetProperty("backoff_ms", out var backoffEl) && backoffEl.ValueKind == JsonValueKind.Array) { foreach (var item in backoffEl.EnumerateArray()) { if (item.TryGetInt32(out var backoffValue)) config.BackOffMs.Add(Math.Max(backoffValue, 0)); } } if (root.TryGetProperty("ack_policy", out var ackPolicyEl)) { var ackPolicy = ackPolicyEl.GetString(); if (string.Equals(ackPolicy, "explicit", StringComparison.OrdinalIgnoreCase)) config.AckPolicy = AckPolicy.Explicit; else if (string.Equals(ackPolicy, "all", StringComparison.OrdinalIgnoreCase)) config.AckPolicy = AckPolicy.All; } if (root.TryGetProperty("deliver_policy", out var deliverPolicyEl)) { var deliver = deliverPolicyEl.GetString(); if (string.Equals(deliver, "last", StringComparison.OrdinalIgnoreCase)) config.DeliverPolicy = DeliverPolicy.Last; else if (string.Equals(deliver, "new", StringComparison.OrdinalIgnoreCase)) config.DeliverPolicy = DeliverPolicy.New; else if (string.Equals(deliver, "by_start_sequence", StringComparison.OrdinalIgnoreCase)) config.DeliverPolicy = DeliverPolicy.ByStartSequence; else if (string.Equals(deliver, "by_start_time", StringComparison.OrdinalIgnoreCase)) config.DeliverPolicy = DeliverPolicy.ByStartTime; else if (string.Equals(deliver, "last_per_subject", StringComparison.OrdinalIgnoreCase)) config.DeliverPolicy = DeliverPolicy.LastPerSubject; } if (root.TryGetProperty("replay_policy", out var replayPolicyEl)) { var replay = replayPolicyEl.GetString(); if (string.Equals(replay, "original", StringComparison.OrdinalIgnoreCase)) config.ReplayPolicy = ReplayPolicy.Original; } return config; } catch (JsonException) { return new ConsumerConfig(); } } private static int ParseBatch(ReadOnlySpan payload) { if (payload.IsEmpty) return 1; try { using var doc = JsonDocument.Parse(payload.ToArray()); if (doc.RootElement.TryGetProperty("batch", out var batchEl) && batchEl.TryGetInt32(out var batch)) return Math.Max(batch, 1); } catch (JsonException) { } return 1; } private static bool ParsePause(ReadOnlySpan payload) { if (payload.IsEmpty) return false; try { using var doc = JsonDocument.Parse(payload.ToArray()); if (doc.RootElement.TryGetProperty("pause", out var pauseEl)) return pauseEl.ValueKind == JsonValueKind.True; } catch (JsonException) { } return false; } private static string? ParseStreamSubject(string subject, string prefix) { if (!subject.StartsWith(prefix, StringComparison.Ordinal)) return null; var stream = subject[prefix.Length..].Trim(); return stream.Length == 0 ? null : stream; } }