using System.Text.Json; using Microsoft.Extensions.Logging; using StellaOps.Messaging.Abstractions; using StackExchange.Redis; namespace StellaOps.Messaging.Transport.Valkey; /// /// Valkey/Redis implementation of . /// Uses set commands (SADD, SMEMBERS, SISMEMBER, SREM, etc.). /// public sealed class ValkeySetStore : ISetStore where TKey : notnull { private readonly ValkeyConnectionFactory _connectionFactory; private readonly string _name; private readonly ILogger>? _logger; private readonly JsonSerializerOptions _jsonOptions; private readonly Func _keySerializer; public ValkeySetStore( ValkeyConnectionFactory connectionFactory, string name, ILogger>? logger = null, JsonSerializerOptions? jsonOptions = null, Func? keySerializer = null) { _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); _name = name ?? throw new ArgumentNullException(nameof(name)); _logger = logger; _jsonOptions = jsonOptions ?? new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false }; _keySerializer = keySerializer ?? (key => key?.ToString() ?? throw new ArgumentNullException(nameof(key))); } /// public string ProviderName => "valkey"; /// public async ValueTask AddAsync( TKey setKey, TElement element, CancellationToken cancellationToken = default) { var redisKey = BuildKey(setKey); var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false); var serialized = Serialize(element); return await db.SetAddAsync(redisKey, serialized).ConfigureAwait(false); } /// public async ValueTask AddRangeAsync( TKey setKey, IEnumerable elements, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(elements); var redisKey = BuildKey(setKey); var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false); var values = elements.Select(e => (RedisValue)Serialize(e)).ToArray(); if (values.Length == 0) { return 0; } return await db.SetAddAsync(redisKey, values).ConfigureAwait(false); } /// public async ValueTask> GetMembersAsync( TKey setKey, CancellationToken cancellationToken = default) { var redisKey = BuildKey(setKey); var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false); var members = await db.SetMembersAsync(redisKey).ConfigureAwait(false); var result = new HashSet(); foreach (var member in members) { if (!member.IsNullOrEmpty) { var element = Deserialize((string)member!); if (element is not null) { result.Add(element); } } } return result; } /// public async ValueTask ContainsAsync( TKey setKey, TElement element, CancellationToken cancellationToken = default) { var redisKey = BuildKey(setKey); var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false); var serialized = Serialize(element); return await db.SetContainsAsync(redisKey, serialized).ConfigureAwait(false); } /// public async ValueTask RemoveAsync( TKey setKey, TElement element, CancellationToken cancellationToken = default) { var redisKey = BuildKey(setKey); var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false); var serialized = Serialize(element); return await db.SetRemoveAsync(redisKey, serialized).ConfigureAwait(false); } /// public async ValueTask RemoveRangeAsync( TKey setKey, IEnumerable elements, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(elements); var redisKey = BuildKey(setKey); var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false); var values = elements.Select(e => (RedisValue)Serialize(e)).ToArray(); if (values.Length == 0) { return 0; } return await db.SetRemoveAsync(redisKey, values).ConfigureAwait(false); } /// public async ValueTask DeleteAsync( TKey setKey, CancellationToken cancellationToken = default) { var redisKey = BuildKey(setKey); var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false); return await db.KeyDeleteAsync(redisKey).ConfigureAwait(false); } /// public async ValueTask CountAsync( TKey setKey, CancellationToken cancellationToken = default) { var redisKey = BuildKey(setKey); var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false); return await db.SetLengthAsync(redisKey).ConfigureAwait(false); } /// public async ValueTask SetExpirationAsync( TKey setKey, TimeSpan ttl, CancellationToken cancellationToken = default) { var redisKey = BuildKey(setKey); var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false); await db.KeyExpireAsync(redisKey, ttl).ConfigureAwait(false); } private string BuildKey(TKey setKey) { var keyString = _keySerializer(setKey); return $"set:{_name}:{keyString}"; } private string Serialize(TElement element) { // For primitive types, use ToString directly if (element is string s) { return s; } return JsonSerializer.Serialize(element, _jsonOptions); } private TElement? Deserialize(string value) { // For string types, return directly if (typeof(TElement) == typeof(string)) { return (TElement)(object)value; } return JsonSerializer.Deserialize(value, _jsonOptions); } } /// /// Factory for creating Valkey set store instances. /// public sealed class ValkeySetStoreFactory : ISetStoreFactory { private readonly ValkeyConnectionFactory _connectionFactory; private readonly ILoggerFactory? _loggerFactory; private readonly JsonSerializerOptions? _jsonOptions; public ValkeySetStoreFactory( ValkeyConnectionFactory connectionFactory, ILoggerFactory? loggerFactory = null, JsonSerializerOptions? jsonOptions = null) { _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); _loggerFactory = loggerFactory; _jsonOptions = jsonOptions; } /// public string ProviderName => "valkey"; /// public ISetStore Create(string name) where TKey : notnull { ArgumentNullException.ThrowIfNull(name); return new ValkeySetStore( _connectionFactory, name, _loggerFactory?.CreateLogger>(), _jsonOptions); } }