feat: complete remaining jetstream parity implementation plan

This commit is contained in:
Joseph Doherty
2026-02-23 10:16:16 -05:00
parent c7bbf45c8f
commit f46b331921
59 changed files with 1734 additions and 54 deletions

View File

@@ -1,12 +1,20 @@
using System.Text.Json;
using System.Text;
using NATS.Server.JetStream.Models;
namespace NATS.Server.JetStream.Api.Handlers;
public static class StreamApiHandlers
{
private const string CreatePrefix = "$JS.API.STREAM.CREATE.";
private const string InfoPrefix = "$JS.API.STREAM.INFO.";
private const string CreatePrefix = JetStreamApiSubjects.StreamCreate;
private const string InfoPrefix = JetStreamApiSubjects.StreamInfo;
private const string UpdatePrefix = JetStreamApiSubjects.StreamUpdate;
private const string DeletePrefix = JetStreamApiSubjects.StreamDelete;
private const string PurgePrefix = JetStreamApiSubjects.StreamPurge;
private const string MessageGetPrefix = JetStreamApiSubjects.StreamMessageGet;
private const string MessageDeletePrefix = JetStreamApiSubjects.StreamMessageDelete;
private const string SnapshotPrefix = JetStreamApiSubjects.StreamSnapshot;
private const string RestorePrefix = JetStreamApiSubjects.StreamRestore;
public static JetStreamApiResponse HandleCreate(string subject, ReadOnlySpan<byte> payload, StreamManager streamManager)
{
@@ -33,6 +41,131 @@ public static class StreamApiHandlers
return streamManager.GetInfo(streamName);
}
public static JetStreamApiResponse HandleUpdate(string subject, ReadOnlySpan<byte> payload, StreamManager streamManager)
{
var streamName = ExtractTrailingToken(subject, UpdatePrefix);
if (streamName == null)
return JetStreamApiResponse.NotFound(subject);
var config = ParseConfig(payload);
if (string.IsNullOrWhiteSpace(config.Name))
config.Name = streamName;
if (config.Subjects.Count == 0)
config.Subjects.Add(streamName.ToLowerInvariant() + ".>");
return streamManager.CreateOrUpdate(config);
}
public static JetStreamApiResponse HandleDelete(string subject, StreamManager streamManager)
{
var streamName = ExtractTrailingToken(subject, DeletePrefix);
if (streamName == null)
return JetStreamApiResponse.NotFound(subject);
return streamManager.Delete(streamName)
? JetStreamApiResponse.SuccessResponse()
: JetStreamApiResponse.NotFound(subject);
}
public static JetStreamApiResponse HandlePurge(string subject, StreamManager streamManager)
{
var streamName = ExtractTrailingToken(subject, PurgePrefix);
if (streamName == null)
return JetStreamApiResponse.NotFound(subject);
return streamManager.Purge(streamName)
? JetStreamApiResponse.SuccessResponse()
: JetStreamApiResponse.NotFound(subject);
}
public static JetStreamApiResponse HandleNames(StreamManager streamManager)
{
return new JetStreamApiResponse
{
StreamNames = streamManager.ListNames(),
};
}
public static JetStreamApiResponse HandleList(StreamManager streamManager)
{
return HandleNames(streamManager);
}
public static JetStreamApiResponse HandleMessageGet(string subject, ReadOnlySpan<byte> payload, StreamManager streamManager)
{
var streamName = ExtractTrailingToken(subject, MessageGetPrefix);
if (streamName == null)
return JetStreamApiResponse.NotFound(subject);
var sequence = ParseSequence(payload);
if (sequence == 0)
return JetStreamApiResponse.ErrorResponse(400, "sequence required");
var message = streamManager.GetMessage(streamName, sequence);
if (message == null)
return JetStreamApiResponse.NotFound(subject);
return new JetStreamApiResponse
{
StreamMessage = new JetStreamStreamMessage
{
Sequence = message.Sequence,
Subject = message.Subject,
Payload = Encoding.UTF8.GetString(message.Payload.Span),
},
};
}
public static JetStreamApiResponse HandleMessageDelete(string subject, ReadOnlySpan<byte> payload, StreamManager streamManager)
{
var streamName = ExtractTrailingToken(subject, MessageDeletePrefix);
if (streamName == null)
return JetStreamApiResponse.NotFound(subject);
var sequence = ParseSequence(payload);
if (sequence == 0)
return JetStreamApiResponse.ErrorResponse(400, "sequence required");
return streamManager.DeleteMessage(streamName, sequence)
? JetStreamApiResponse.SuccessResponse()
: JetStreamApiResponse.NotFound(subject);
}
public static JetStreamApiResponse HandleSnapshot(string subject, StreamManager streamManager)
{
var streamName = ExtractTrailingToken(subject, SnapshotPrefix);
if (streamName == null)
return JetStreamApiResponse.NotFound(subject);
var snapshot = streamManager.CreateSnapshot(streamName);
if (snapshot == null)
return JetStreamApiResponse.NotFound(subject);
return new JetStreamApiResponse
{
Snapshot = new JetStreamSnapshot
{
Payload = Convert.ToBase64String(snapshot),
},
};
}
public static JetStreamApiResponse HandleRestore(string subject, ReadOnlySpan<byte> payload, StreamManager streamManager)
{
var streamName = ExtractTrailingToken(subject, RestorePrefix);
if (streamName == null)
return JetStreamApiResponse.NotFound(subject);
var snapshotBytes = ParseRestorePayload(payload);
if (snapshotBytes == null)
return JetStreamApiResponse.ErrorResponse(400, "snapshot payload required");
return streamManager.RestoreSnapshot(streamName, snapshotBytes)
? JetStreamApiResponse.SuccessResponse()
: JetStreamApiResponse.NotFound(subject);
}
private static string? ExtractTrailingToken(string subject, string prefix)
{
if (!subject.StartsWith(prefix, StringComparison.Ordinal))
@@ -88,4 +221,56 @@ public static class StreamApiHandlers
return new StreamConfig();
}
}
private static ulong ParseSequence(ReadOnlySpan<byte> payload)
{
if (payload.IsEmpty)
return 0;
try
{
using var doc = JsonDocument.Parse(payload.ToArray());
if (doc.RootElement.TryGetProperty("seq", out var seqEl) && seqEl.TryGetUInt64(out var sequence))
return sequence;
}
catch (JsonException)
{
}
return 0;
}
private static byte[]? ParseRestorePayload(ReadOnlySpan<byte> payload)
{
if (payload.IsEmpty)
return null;
var raw = Encoding.UTF8.GetString(payload).Trim();
if (raw.Length == 0)
return null;
try
{
return Convert.FromBase64String(raw);
}
catch (FormatException)
{
}
try
{
using var doc = JsonDocument.Parse(payload.ToArray());
if (doc.RootElement.TryGetProperty("payload", out var payloadEl))
{
var base64 = payloadEl.GetString();
if (!string.IsNullOrWhiteSpace(base64))
return Convert.FromBase64String(base64);
}
}
catch (JsonException)
{
}
return null;
}
}