using NATS.Server.JetStream.Models; namespace NATS.Server.JetStream.Api; public sealed class JetStreamApiResponse { public JetStreamApiError? Error { get; init; } public JetStreamStreamInfo? StreamInfo { get; init; } public JetStreamConsumerInfo? ConsumerInfo { get; init; } public JetStreamAccountInfo? AccountInfo { get; init; } public IReadOnlyList? StreamNames { get; init; } public IReadOnlyList? StreamInfoList { get; init; } public IReadOnlyList? ConsumerNames { get; init; } public IReadOnlyList? ConsumerInfoList { get; init; } public JetStreamStreamMessage? StreamMessage { get; init; } public JetStreamDirectMessage? DirectMessage { get; init; } public JetStreamSnapshot? Snapshot { get; init; } public JetStreamPullBatch? PullBatch { get; init; } public bool Success { get; init; } public ulong Purged { get; init; } /// /// Total count of all items (before pagination). Used by list responses for offset-based pagination. /// Go reference: jetstream_api.go — ApiPaged struct includes Total, Offset, Limit fields. /// public int PaginationTotal { get; init; } /// /// Requested offset for pagination. Echoed back to client so it can calculate the next page. /// public int PaginationOffset { get; init; } /// /// Whether the consumer is currently paused. Populated by pause/resume API responses. /// Go reference: server/consumer.go jsConsumerPauseResponse.paused field. /// public bool? Paused { get; init; } /// /// UTC deadline until which the consumer is paused. Null when no deadline is set. /// Go reference: server/consumer.go jsConsumerPauseResponse.pause_until field. /// public DateTime? PauseUntil { get; init; } /// /// Returns a wire-format object for JSON serialization matching the Go server's /// flat response structure (e.g., config/state at root level for stream responses, /// not nested under a wrapper property). /// public object ToWireFormat() { if (StreamInfo != null) { if (Error != null) return new { type = "io.nats.jetstream.api.v1.stream_create_response", error = Error }; return new { type = "io.nats.jetstream.api.v1.stream_create_response", config = ToWireConfig(StreamInfo.Config), state = ToWireState(StreamInfo.State), }; } if (ConsumerInfo != null) { if (Error != null) return new { type = "io.nats.jetstream.api.v1.consumer_create_response", error = Error }; return new { type = "io.nats.jetstream.api.v1.consumer_create_response", stream_name = ConsumerInfo.StreamName, name = ConsumerInfo.Name, config = ToWireConsumerConfig(ConsumerInfo.Config), }; } if (Error != null) return new { error = Error }; if (StreamInfoList != null) { var wireStreams = StreamInfoList.Select(s => new { config = ToWireConfig(s.Config), state = ToWireState(s.State), }).ToList(); return new { total = PaginationTotal, offset = PaginationOffset, limit = wireStreams.Count, streams = wireStreams }; } if (StreamNames != null) return new { total = PaginationTotal, offset = PaginationOffset, limit = StreamNames.Count, streams = StreamNames }; if (ConsumerInfoList != null) { var wireConsumers = ConsumerInfoList.Select(c => new { stream_name = c.StreamName, name = c.Name, config = ToWireConsumerConfig(c.Config), }).ToList(); return new { total = PaginationTotal, offset = PaginationOffset, limit = wireConsumers.Count, consumers = wireConsumers }; } if (ConsumerNames != null) return new { total = PaginationTotal, offset = PaginationOffset, limit = ConsumerNames.Count, consumers = ConsumerNames }; if (Purged > 0 || Success) return new { success = Success, purged = Purged }; return new { success = Success }; } /// /// Creates a Go-compatible wire format for StreamConfig. /// Only includes fields the Go server sends, with enums as lowercase strings. /// Go reference: server/stream.go StreamConfig JSON marshaling. /// private static object ToWireConfig(StreamConfig c) => new { name = c.Name, subjects = c.Subjects, retention = c.Retention.ToString().ToLowerInvariant(), max_consumers = c.MaxConsumers, max_msgs = c.MaxMsgs, max_bytes = c.MaxBytes, max_age = c.MaxAge, max_msgs_per_subject = c.MaxMsgsPer, max_msg_size = c.MaxMsgSize, storage = c.Storage.ToString().ToLowerInvariant(), discard = c.Discard.ToString().ToLowerInvariant(), num_replicas = c.Replicas, duplicate_window = (long)c.DuplicateWindowMs * 1_000_000L, sealed_field = c.Sealed, deny_delete = c.DenyDelete, deny_purge = c.DenyPurge, allow_direct = c.AllowDirect, first_seq = c.FirstSeq, }; private static object ToWireState(ApiStreamState s) => new { messages = s.Messages, bytes = s.Bytes, first_seq = s.FirstSeq, last_seq = s.LastSeq, consumer_count = 0, }; private static object ToWireConsumerConfig(ConsumerConfig c) => new { durable_name = string.IsNullOrEmpty(c.DurableName) ? null : c.DurableName, name = string.IsNullOrEmpty(c.DurableName) ? null : c.DurableName, deliver_policy = c.DeliverPolicy.ToString().ToLowerInvariant(), ack_policy = c.AckPolicy.ToString().ToLowerInvariant(), replay_policy = c.ReplayPolicy.ToString().ToLowerInvariant(), ack_wait = (long)c.AckWaitMs * 1_000_000L, max_deliver = c.MaxDeliver, max_ack_pending = c.MaxAckPending, filter_subject = c.FilterSubject, // Go: consumer.go — deliver_subject present for push consumers deliver_subject = string.IsNullOrEmpty(c.DeliverSubject) ? null : c.DeliverSubject, }; public static JetStreamApiResponse NotFound(string subject) => new() { Error = new JetStreamApiError { Code = 404, Description = $"unknown api subject '{subject}'", }, }; public static JetStreamApiResponse Ok() => new(); public static JetStreamApiResponse SuccessResponse() => new() { Success = true, }; public static JetStreamApiResponse ErrorResponse(int code, string description) => new() { Error = new JetStreamApiError { Code = code, Description = description, }, }; /// /// Returns a not-leader error with code 10003 and a leader_hint. /// Go reference: jetstream_api.go:200-300 — non-leader nodes return this error /// for mutating operations so clients can redirect. /// public static JetStreamApiResponse NotLeader(string leaderHint) => new() { Error = new JetStreamApiError { Code = 10003, Description = "not leader", LeaderHint = leaderHint, }, }; /// /// Returns a purge success response with the number of messages purged. /// Go reference: jetstream_api.go:1200-1350 — purge response includes purged count. /// public static JetStreamApiResponse PurgeResponse(ulong purged) => new() { Success = true, Purged = purged, }; /// /// Returns a pause/resume success response with current pause state. /// Go reference: server/consumer.go jsConsumerPauseResponse — returned after pause/resume API call. /// public static JetStreamApiResponse PauseResponse(bool paused, DateTime? pauseUntil) => new() { Success = true, Paused = paused, PauseUntil = pauseUntil, }; } public sealed class JetStreamStreamInfo { public required StreamConfig Config { get; init; } public required ApiStreamState State { get; init; } } public sealed class JetStreamConsumerInfo { public string? Name { get; init; } public string? StreamName { get; init; } public required ConsumerConfig Config { get; init; } } public sealed class JetStreamAccountInfo { public int Streams { get; init; } public int Consumers { get; init; } } public sealed class JetStreamStreamMessage { public ulong Sequence { get; init; } public string Subject { get; init; } = string.Empty; public string Payload { get; init; } = string.Empty; } public sealed class JetStreamDirectMessage { public ulong Sequence { get; init; } public string Subject { get; init; } = string.Empty; public string Payload { get; init; } = string.Empty; } public sealed class JetStreamSnapshot { public string Payload { get; init; } = string.Empty; /// Stream name this snapshot was taken from. public string? StreamName { get; init; } /// Number of chunks the snapshot was split into (1 for non-chunked snapshots). public int NumChunks { get; init; } /// Block/chunk size in bytes. public int BlkSize { get; init; } } public sealed class JetStreamPullBatch { public IReadOnlyList Messages { get; init; } = []; }