This commit is contained in:
StellaOps Bot
2025-12-13 02:22:15 +02:00
parent 564df71bfb
commit 999e26a48e
395 changed files with 25045 additions and 2224 deletions

View File

@@ -0,0 +1,78 @@
namespace StellaOps.Messaging.Abstractions;
/// <summary>
/// Transport-agnostic atomic token store for one-time consumable tokens.
/// Supports issuing tokens with TTL and atomic consumption (single use).
/// </summary>
/// <typeparam name="TPayload">The type of metadata payload stored with the token.</typeparam>
public interface IAtomicTokenStore<TPayload>
{
/// <summary>
/// Gets the provider name for diagnostics (e.g., "valkey", "postgres", "inmemory").
/// </summary>
string ProviderName { get; }
/// <summary>
/// Issues a token with the given payload and TTL.
/// </summary>
/// <param name="key">The storage key for the token.</param>
/// <param name="payload">The metadata payload to store with the token.</param>
/// <param name="ttl">The time-to-live for the token.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The result containing the generated token.</returns>
ValueTask<TokenIssueResult> IssueAsync(
string key,
TPayload payload,
TimeSpan ttl,
CancellationToken cancellationToken = default);
/// <summary>
/// Stores a caller-provided token with payload and TTL.
/// Use when the token must be generated externally (e.g., cryptographic nonces).
/// </summary>
/// <param name="key">The storage key for the token.</param>
/// <param name="token">The caller-provided token value.</param>
/// <param name="payload">The metadata payload to store with the token.</param>
/// <param name="ttl">The time-to-live for the token.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The result containing the stored token information.</returns>
ValueTask<TokenIssueResult> StoreAsync(
string key,
string token,
TPayload payload,
TimeSpan ttl,
CancellationToken cancellationToken = default);
/// <summary>
/// Atomically consumes a token if it exists and matches.
/// The token is deleted after successful consumption (single use).
/// </summary>
/// <param name="key">The storage key for the token.</param>
/// <param name="expectedToken">The token value to match.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The result of the consumption attempt.</returns>
ValueTask<TokenConsumeResult<TPayload>> TryConsumeAsync(
string key,
string expectedToken,
CancellationToken cancellationToken = default);
/// <summary>
/// Checks if a token exists without consuming it.
/// </summary>
/// <param name="key">The storage key for the token.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the token exists.</returns>
ValueTask<bool> ExistsAsync(
string key,
CancellationToken cancellationToken = default);
/// <summary>
/// Revokes a token before it expires.
/// </summary>
/// <param name="key">The storage key for the token.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the token existed and was revoked.</returns>
ValueTask<bool> RevokeAsync(
string key,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,74 @@
namespace StellaOps.Messaging.Abstractions;
/// <summary>
/// Transport-agnostic event stream interface.
/// Provides fire-and-forget event publishing without consumer group semantics.
/// Unlike <see cref="IMessageQueue{TMessage}"/>, events are not acknowledged and may be consumed by multiple subscribers.
/// </summary>
/// <typeparam name="TEvent">The event type.</typeparam>
public interface IEventStream<TEvent> where TEvent : class
{
/// <summary>
/// Gets the provider name for diagnostics (e.g., "valkey", "postgres", "inmemory").
/// </summary>
string ProviderName { get; }
/// <summary>
/// Gets the stream name.
/// </summary>
string StreamName { get; }
/// <summary>
/// Publishes an event to the stream.
/// </summary>
/// <param name="event">The event to publish.</param>
/// <param name="options">Optional publish options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The result of the publish operation.</returns>
ValueTask<EventPublishResult> PublishAsync(
TEvent @event,
EventPublishOptions? options = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Publishes multiple events to the stream.
/// </summary>
/// <param name="events">The events to publish.</param>
/// <param name="options">Optional publish options (applied to all events).</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The results of the publish operations.</returns>
ValueTask<IReadOnlyList<EventPublishResult>> PublishBatchAsync(
IEnumerable<TEvent> events,
EventPublishOptions? options = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Subscribes to events from a position.
/// Events are delivered as they become available.
/// </summary>
/// <param name="position">The stream position to start from.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An async enumerable of events.</returns>
IAsyncEnumerable<StreamEvent<TEvent>> SubscribeAsync(
StreamPosition position,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets stream metadata.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Information about the stream.</returns>
ValueTask<StreamInfo> GetInfoAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Trims stream to approximate max length.
/// </summary>
/// <param name="maxLength">The maximum length to retain.</param>
/// <param name="approximate">Whether to use approximate trimming (more efficient).</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The number of entries removed.</returns>
ValueTask<long> TrimAsync(
long maxLength,
bool approximate = true,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,70 @@
namespace StellaOps.Messaging.Abstractions;
/// <summary>
/// Transport-agnostic idempotency store interface.
/// Provides deduplication keys with configurable time windows.
/// </summary>
public interface IIdempotencyStore
{
/// <summary>
/// Gets the provider name for diagnostics (e.g., "valkey", "postgres", "inmemory").
/// </summary>
string ProviderName { get; }
/// <summary>
/// Attempts to claim an idempotency key.
/// If the key doesn't exist, it's claimed for the duration of the window.
/// </summary>
/// <param name="key">The idempotency key.</param>
/// <param name="value">The value to store (e.g., message ID, operation ID).</param>
/// <param name="window">The idempotency window duration.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The result indicating whether this was the first claim.</returns>
ValueTask<IdempotencyResult> TryClaimAsync(
string key,
string value,
TimeSpan window,
CancellationToken cancellationToken = default);
/// <summary>
/// Checks if a key was already claimed.
/// </summary>
/// <param name="key">The idempotency key.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the key exists (was previously claimed).</returns>
ValueTask<bool> ExistsAsync(
string key,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets the value for a claimed key.
/// </summary>
/// <param name="key">The idempotency key.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The stored value, or null if the key doesn't exist.</returns>
ValueTask<string?> GetAsync(
string key,
CancellationToken cancellationToken = default);
/// <summary>
/// Releases a claimed key before the window expires.
/// </summary>
/// <param name="key">The idempotency key.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the key existed and was released.</returns>
ValueTask<bool> ReleaseAsync(
string key,
CancellationToken cancellationToken = default);
/// <summary>
/// Extends the window for a claimed key.
/// </summary>
/// <param name="key">The idempotency key.</param>
/// <param name="extension">The time to extend by.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the key existed and was extended.</returns>
ValueTask<bool> ExtendAsync(
string key,
TimeSpan extension,
CancellationToken cancellationToken = default);
}

View File

@@ -46,3 +46,120 @@ public interface IDistributedCacheFactory
/// <returns>A configured distributed cache instance.</returns>
IDistributedCache<TValue> Create<TValue>(CacheOptions options);
}
/// <summary>
/// Factory for creating rate limiter instances.
/// </summary>
public interface IRateLimiterFactory
{
/// <summary>
/// Gets the provider name for this factory.
/// </summary>
string ProviderName { get; }
/// <summary>
/// Creates a rate limiter with the specified name.
/// </summary>
/// <param name="name">The rate limiter name (used as key prefix).</param>
/// <returns>A configured rate limiter instance.</returns>
IRateLimiter Create(string name);
}
/// <summary>
/// Factory for creating atomic token store instances.
/// </summary>
public interface IAtomicTokenStoreFactory
{
/// <summary>
/// Gets the provider name for this factory.
/// </summary>
string ProviderName { get; }
/// <summary>
/// Creates an atomic token store for the specified payload type.
/// </summary>
/// <typeparam name="TPayload">The payload type.</typeparam>
/// <param name="name">The store name (used as key prefix).</param>
/// <returns>A configured atomic token store instance.</returns>
IAtomicTokenStore<TPayload> Create<TPayload>(string name);
}
/// <summary>
/// Factory for creating sorted index instances.
/// </summary>
public interface ISortedIndexFactory
{
/// <summary>
/// Gets the provider name for this factory.
/// </summary>
string ProviderName { get; }
/// <summary>
/// Creates a sorted index for the specified key and element types.
/// </summary>
/// <typeparam name="TKey">The index key type.</typeparam>
/// <typeparam name="TElement">The element type.</typeparam>
/// <param name="name">The index name (used as key prefix).</param>
/// <returns>A configured sorted index instance.</returns>
ISortedIndex<TKey, TElement> Create<TKey, TElement>(string name)
where TKey : notnull
where TElement : notnull;
}
/// <summary>
/// Factory for creating set store instances.
/// </summary>
public interface ISetStoreFactory
{
/// <summary>
/// Gets the provider name for this factory.
/// </summary>
string ProviderName { get; }
/// <summary>
/// Creates a set store for the specified key and element types.
/// </summary>
/// <typeparam name="TKey">The set key type.</typeparam>
/// <typeparam name="TElement">The element type.</typeparam>
/// <param name="name">The store name (used as key prefix).</param>
/// <returns>A configured set store instance.</returns>
ISetStore<TKey, TElement> Create<TKey, TElement>(string name)
where TKey : notnull;
}
/// <summary>
/// Factory for creating event stream instances.
/// </summary>
public interface IEventStreamFactory
{
/// <summary>
/// Gets the provider name for this factory.
/// </summary>
string ProviderName { get; }
/// <summary>
/// Creates an event stream for the specified event type.
/// </summary>
/// <typeparam name="TEvent">The event type.</typeparam>
/// <param name="options">The event stream options.</param>
/// <returns>A configured event stream instance.</returns>
IEventStream<TEvent> Create<TEvent>(EventStreamOptions options) where TEvent : class;
}
/// <summary>
/// Factory for creating idempotency store instances.
/// </summary>
public interface IIdempotencyStoreFactory
{
/// <summary>
/// Gets the provider name for this factory.
/// </summary>
string ProviderName { get; }
/// <summary>
/// Creates an idempotency store with the specified name.
/// </summary>
/// <param name="name">The store name (used as key prefix).</param>
/// <returns>A configured idempotency store instance.</returns>
IIdempotencyStore Create(string name);
}

View File

@@ -0,0 +1,47 @@
namespace StellaOps.Messaging.Abstractions;
/// <summary>
/// Transport-agnostic rate limiter interface.
/// Implements sliding window rate limiting with configurable policies.
/// </summary>
public interface IRateLimiter
{
/// <summary>
/// Gets the provider name for diagnostics (e.g., "valkey", "postgres", "inmemory").
/// </summary>
string ProviderName { get; }
/// <summary>
/// Attempts to acquire a permit from the rate limiter.
/// </summary>
/// <param name="key">The rate limit key (e.g., user ID, IP address, resource identifier).</param>
/// <param name="policy">The rate limit policy defining max permits and window.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The result of the rate limit check.</returns>
ValueTask<RateLimitResult> TryAcquireAsync(
string key,
RateLimitPolicy policy,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets current usage for a key without consuming a permit.
/// </summary>
/// <param name="key">The rate limit key.</param>
/// <param name="policy">The rate limit policy.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The current rate limit status.</returns>
ValueTask<RateLimitStatus> GetStatusAsync(
string key,
RateLimitPolicy policy,
CancellationToken cancellationToken = default);
/// <summary>
/// Resets the rate limit counter for a key.
/// </summary>
/// <param name="key">The rate limit key.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the key existed and was reset.</returns>
ValueTask<bool> ResetAsync(
string key,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,116 @@
namespace StellaOps.Messaging.Abstractions;
/// <summary>
/// Transport-agnostic set store interface.
/// Provides unordered set membership operations.
/// </summary>
/// <typeparam name="TKey">The set key type.</typeparam>
/// <typeparam name="TElement">The element type stored in the set.</typeparam>
public interface ISetStore<TKey, TElement>
{
/// <summary>
/// Gets the provider name for diagnostics (e.g., "valkey", "postgres", "inmemory").
/// </summary>
string ProviderName { get; }
/// <summary>
/// Adds an element to the set.
/// </summary>
/// <param name="setKey">The set key.</param>
/// <param name="element">The element to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the element was added (false if already existed).</returns>
ValueTask<bool> AddAsync(
TKey setKey,
TElement element,
CancellationToken cancellationToken = default);
/// <summary>
/// Adds multiple elements to the set.
/// </summary>
/// <param name="setKey">The set key.</param>
/// <param name="elements">The elements to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The number of elements added (not already present).</returns>
ValueTask<long> AddRangeAsync(
TKey setKey,
IEnumerable<TElement> elements,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets all members of the set.
/// </summary>
/// <param name="setKey">The set key.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>All elements in the set.</returns>
ValueTask<IReadOnlySet<TElement>> GetMembersAsync(
TKey setKey,
CancellationToken cancellationToken = default);
/// <summary>
/// Checks if an element exists in the set.
/// </summary>
/// <param name="setKey">The set key.</param>
/// <param name="element">The element to check.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the element is a member of the set.</returns>
ValueTask<bool> ContainsAsync(
TKey setKey,
TElement element,
CancellationToken cancellationToken = default);
/// <summary>
/// Removes an element from the set.
/// </summary>
/// <param name="setKey">The set key.</param>
/// <param name="element">The element to remove.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the element was removed.</returns>
ValueTask<bool> RemoveAsync(
TKey setKey,
TElement element,
CancellationToken cancellationToken = default);
/// <summary>
/// Removes multiple elements from the set.
/// </summary>
/// <param name="setKey">The set key.</param>
/// <param name="elements">The elements to remove.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The number of elements removed.</returns>
ValueTask<long> RemoveRangeAsync(
TKey setKey,
IEnumerable<TElement> elements,
CancellationToken cancellationToken = default);
/// <summary>
/// Deletes the entire set.
/// </summary>
/// <param name="setKey">The set key.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the set existed and was deleted.</returns>
ValueTask<bool> DeleteAsync(
TKey setKey,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets the cardinality (count) of the set.
/// </summary>
/// <param name="setKey">The set key.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The number of elements in the set.</returns>
ValueTask<long> CountAsync(
TKey setKey,
CancellationToken cancellationToken = default);
/// <summary>
/// Sets TTL on the set key.
/// </summary>
/// <param name="setKey">The set key.</param>
/// <param name="ttl">The time-to-live.</param>
/// <param name="cancellationToken">Cancellation token.</param>
ValueTask SetExpirationAsync(
TKey setKey,
TimeSpan ttl,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,180 @@
namespace StellaOps.Messaging.Abstractions;
/// <summary>
/// Transport-agnostic sorted index interface.
/// Provides score-ordered collections with range queries.
/// </summary>
/// <typeparam name="TKey">The index key type.</typeparam>
/// <typeparam name="TElement">The element type stored in the index.</typeparam>
public interface ISortedIndex<TKey, TElement>
{
/// <summary>
/// Gets the provider name for diagnostics (e.g., "valkey", "postgres", "inmemory").
/// </summary>
string ProviderName { get; }
/// <summary>
/// Adds an element with a score.
/// </summary>
/// <param name="indexKey">The index key.</param>
/// <param name="element">The element to add.</param>
/// <param name="score">The score for ordering.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the element was added (false if updated).</returns>
ValueTask<bool> AddAsync(
TKey indexKey,
TElement element,
double score,
CancellationToken cancellationToken = default);
/// <summary>
/// Adds multiple elements with scores atomically.
/// </summary>
/// <param name="indexKey">The index key.</param>
/// <param name="elements">The elements with scores to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The number of elements added (not updated).</returns>
ValueTask<long> AddRangeAsync(
TKey indexKey,
IEnumerable<ScoredElement<TElement>> elements,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets elements by rank range (0-based, inclusive).
/// </summary>
/// <param name="indexKey">The index key.</param>
/// <param name="start">The start rank (0-based).</param>
/// <param name="stop">The stop rank (inclusive, use -1 for last).</param>
/// <param name="order">The sort order.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Elements within the rank range.</returns>
ValueTask<IReadOnlyList<ScoredElement<TElement>>> GetByRankAsync(
TKey indexKey,
long start,
long stop,
SortOrder order = SortOrder.Ascending,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets elements by score range.
/// </summary>
/// <param name="indexKey">The index key.</param>
/// <param name="minScore">The minimum score (inclusive).</param>
/// <param name="maxScore">The maximum score (inclusive).</param>
/// <param name="order">The sort order.</param>
/// <param name="limit">Optional limit on returned elements.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Elements within the score range.</returns>
ValueTask<IReadOnlyList<ScoredElement<TElement>>> GetByScoreAsync(
TKey indexKey,
double minScore,
double maxScore,
SortOrder order = SortOrder.Ascending,
int? limit = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets the score of an element.
/// </summary>
/// <param name="indexKey">The index key.</param>
/// <param name="element">The element to look up.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The score, or null if the element doesn't exist.</returns>
ValueTask<double?> GetScoreAsync(
TKey indexKey,
TElement element,
CancellationToken cancellationToken = default);
/// <summary>
/// Removes an element from the index.
/// </summary>
/// <param name="indexKey">The index key.</param>
/// <param name="element">The element to remove.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the element was removed.</returns>
ValueTask<bool> RemoveAsync(
TKey indexKey,
TElement element,
CancellationToken cancellationToken = default);
/// <summary>
/// Removes multiple elements from the index.
/// </summary>
/// <param name="indexKey">The index key.</param>
/// <param name="elements">The elements to remove.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The number of elements removed.</returns>
ValueTask<long> RemoveRangeAsync(
TKey indexKey,
IEnumerable<TElement> elements,
CancellationToken cancellationToken = default);
/// <summary>
/// Removes elements by score range.
/// </summary>
/// <param name="indexKey">The index key.</param>
/// <param name="minScore">The minimum score (inclusive).</param>
/// <param name="maxScore">The maximum score (inclusive).</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The number of elements removed.</returns>
ValueTask<long> RemoveByScoreAsync(
TKey indexKey,
double minScore,
double maxScore,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets the total count of elements in the index.
/// </summary>
/// <param name="indexKey">The index key.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The element count.</returns>
ValueTask<long> CountAsync(
TKey indexKey,
CancellationToken cancellationToken = default);
/// <summary>
/// Deletes the entire index.
/// </summary>
/// <param name="indexKey">The index key.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the index existed and was deleted.</returns>
ValueTask<bool> DeleteAsync(
TKey indexKey,
CancellationToken cancellationToken = default);
/// <summary>
/// Sets TTL on the index key.
/// </summary>
/// <param name="indexKey">The index key.</param>
/// <param name="ttl">The time-to-live.</param>
/// <param name="cancellationToken">Cancellation token.</param>
ValueTask SetExpirationAsync(
TKey indexKey,
TimeSpan ttl,
CancellationToken cancellationToken = default);
}
/// <summary>
/// An element with an associated score.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <param name="Element">The element value.</param>
/// <param name="Score">The score for ordering.</param>
public readonly record struct ScoredElement<T>(T Element, double Score);
/// <summary>
/// Sort order for index queries.
/// </summary>
public enum SortOrder
{
/// <summary>
/// Sort by ascending score (lowest first).
/// </summary>
Ascending,
/// <summary>
/// Sort by descending score (highest first).
/// </summary>
Descending
}

View File

@@ -0,0 +1,42 @@
namespace StellaOps.Messaging;
/// <summary>
/// Configuration options for event streams.
/// </summary>
public sealed class EventStreamOptions
{
/// <summary>
/// Gets or sets the stream name.
/// </summary>
public required string StreamName { get; set; }
/// <summary>
/// Gets or sets the maximum stream length.
/// When set, the stream is automatically trimmed to this approximate length.
/// </summary>
public long? MaxLength { get; set; }
/// <summary>
/// Gets or sets whether to use approximate trimming (more efficient).
/// Default is true.
/// </summary>
public bool ApproximateTrimming { get; set; } = true;
/// <summary>
/// Gets or sets the polling interval for subscription (when applicable).
/// Default is 100ms.
/// </summary>
public TimeSpan PollInterval { get; set; } = TimeSpan.FromMilliseconds(100);
/// <summary>
/// Gets or sets the idempotency window for duplicate detection.
/// Default is 5 minutes.
/// </summary>
public TimeSpan IdempotencyWindow { get; set; } = TimeSpan.FromMinutes(5);
/// <summary>
/// Gets or sets whether idempotency checking is enabled.
/// Default is false.
/// </summary>
public bool EnableIdempotency { get; set; }
}

View File

@@ -0,0 +1,177 @@
namespace StellaOps.Messaging;
/// <summary>
/// Options for publishing events to a stream.
/// </summary>
public sealed record EventPublishOptions
{
/// <summary>
/// Gets or sets the idempotency key for deduplication.
/// </summary>
public string? IdempotencyKey { get; init; }
/// <summary>
/// Gets or sets the tenant identifier.
/// </summary>
public string? TenantId { get; init; }
/// <summary>
/// Gets or sets the correlation identifier for tracing.
/// </summary>
public string? CorrelationId { get; init; }
/// <summary>
/// Gets or sets additional headers.
/// </summary>
public IReadOnlyDictionary<string, string>? Headers { get; init; }
/// <summary>
/// Gets or sets the maximum stream length (triggers trimming).
/// </summary>
public long? MaxStreamLength { get; init; }
}
/// <summary>
/// Result of an event publish operation.
/// </summary>
public readonly struct EventPublishResult
{
/// <summary>
/// Gets whether the publish was successful.
/// </summary>
public bool Success { get; init; }
/// <summary>
/// Gets the entry ID assigned by the stream.
/// </summary>
public string? EntryId { get; init; }
/// <summary>
/// Gets whether this was a duplicate (based on idempotency key).
/// </summary>
public bool WasDeduplicated { get; init; }
/// <summary>
/// Gets the error message if the operation failed.
/// </summary>
public string? Error { get; init; }
/// <summary>
/// Creates a successful publish result.
/// </summary>
public static EventPublishResult Succeeded(string entryId, bool wasDeduplicated = false) =>
new()
{
Success = true,
EntryId = entryId,
WasDeduplicated = wasDeduplicated
};
/// <summary>
/// Creates a failed publish result.
/// </summary>
public static EventPublishResult Failed(string error) =>
new()
{
Success = false,
Error = error
};
/// <summary>
/// Creates a deduplicated result.
/// </summary>
public static EventPublishResult Deduplicated(string existingEntryId) =>
new()
{
Success = true,
EntryId = existingEntryId,
WasDeduplicated = true
};
}
/// <summary>
/// An event from the stream with metadata.
/// </summary>
/// <typeparam name="T">The event type.</typeparam>
/// <param name="EntryId">The stream entry identifier.</param>
/// <param name="Event">The event payload.</param>
/// <param name="Timestamp">When the event was published.</param>
/// <param name="TenantId">The tenant identifier, if present.</param>
/// <param name="CorrelationId">The correlation identifier, if present.</param>
public sealed record StreamEvent<T>(
string EntryId,
T Event,
DateTimeOffset Timestamp,
string? TenantId,
string? CorrelationId);
/// <summary>
/// Represents a position in the stream.
/// </summary>
public readonly struct StreamPosition : IEquatable<StreamPosition>
{
/// <summary>
/// Position at the beginning of the stream (read all).
/// </summary>
public static StreamPosition Beginning => new("0");
/// <summary>
/// Position at the end of the stream (only new entries).
/// </summary>
public static StreamPosition End => new("$");
/// <summary>
/// Creates a position after a specific entry ID.
/// </summary>
public static StreamPosition After(string entryId) => new(entryId);
/// <summary>
/// Gets the position value.
/// </summary>
public string Value { get; }
private StreamPosition(string value) => Value = value;
/// <inheritdoc />
public bool Equals(StreamPosition other) => Value == other.Value;
/// <inheritdoc />
public override bool Equals(object? obj) => obj is StreamPosition other && Equals(other);
/// <inheritdoc />
public override int GetHashCode() => Value?.GetHashCode() ?? 0;
/// <summary>
/// Equality operator.
/// </summary>
public static bool operator ==(StreamPosition left, StreamPosition right) => left.Equals(right);
/// <summary>
/// Inequality operator.
/// </summary>
public static bool operator !=(StreamPosition left, StreamPosition right) => !left.Equals(right);
/// <inheritdoc />
public override string ToString() => Value;
}
/// <summary>
/// Information about a stream.
/// </summary>
/// <param name="Length">The number of entries in the stream.</param>
/// <param name="FirstEntryId">The ID of the first entry, if any.</param>
/// <param name="LastEntryId">The ID of the last entry, if any.</param>
/// <param name="FirstEntryTimestamp">The timestamp of the first entry, if available.</param>
/// <param name="LastEntryTimestamp">The timestamp of the last entry, if available.</param>
public sealed record StreamInfo(
long Length,
string? FirstEntryId,
string? LastEntryId,
DateTimeOffset? FirstEntryTimestamp,
DateTimeOffset? LastEntryTimestamp)
{
/// <summary>
/// Creates an empty stream info.
/// </summary>
public static StreamInfo Empty => new(0, null, null, null, null);
}

View File

@@ -0,0 +1,41 @@
namespace StellaOps.Messaging;
/// <summary>
/// Result of an idempotency claim attempt.
/// </summary>
public readonly struct IdempotencyResult
{
/// <summary>
/// Gets whether this was the first claim (not a duplicate).
/// </summary>
public bool IsFirstClaim { get; init; }
/// <summary>
/// Gets the existing value if this was a duplicate.
/// </summary>
public string? ExistingValue { get; init; }
/// <summary>
/// Gets whether this was a duplicate (key already claimed).
/// </summary>
public bool IsDuplicate => !IsFirstClaim;
/// <summary>
/// Creates a result indicating the key was successfully claimed.
/// </summary>
public static IdempotencyResult Claimed() =>
new()
{
IsFirstClaim = true
};
/// <summary>
/// Creates a result indicating the key was already claimed (duplicate).
/// </summary>
public static IdempotencyResult Duplicate(string existingValue) =>
new()
{
IsFirstClaim = false,
ExistingValue = existingValue
};
}

View File

@@ -0,0 +1,127 @@
namespace StellaOps.Messaging;
/// <summary>
/// Defines a rate limit policy.
/// </summary>
/// <param name="MaxPermits">Maximum number of permits allowed within the window.</param>
/// <param name="Window">The time window for rate limiting.</param>
public sealed record RateLimitPolicy(int MaxPermits, TimeSpan Window)
{
/// <summary>
/// Creates a per-second rate limit policy.
/// </summary>
public static RateLimitPolicy PerSecond(int maxPermits) =>
new(maxPermits, TimeSpan.FromSeconds(1));
/// <summary>
/// Creates a per-minute rate limit policy.
/// </summary>
public static RateLimitPolicy PerMinute(int maxPermits) =>
new(maxPermits, TimeSpan.FromMinutes(1));
/// <summary>
/// Creates a per-hour rate limit policy.
/// </summary>
public static RateLimitPolicy PerHour(int maxPermits) =>
new(maxPermits, TimeSpan.FromHours(1));
}
/// <summary>
/// Result of a rate limit acquisition attempt.
/// </summary>
public readonly struct RateLimitResult
{
/// <summary>
/// Gets whether the permit was acquired (request allowed).
/// </summary>
public bool IsAllowed { get; init; }
/// <summary>
/// Gets the current count of permits used in the window.
/// </summary>
public int CurrentCount { get; init; }
/// <summary>
/// Gets the number of remaining permits in the window.
/// </summary>
public int RemainingPermits { get; init; }
/// <summary>
/// Gets the suggested time to wait before retrying (when denied).
/// </summary>
public TimeSpan? RetryAfter { get; init; }
/// <summary>
/// Creates a result indicating the permit was acquired.
/// </summary>
public static RateLimitResult Allowed(int currentCount, int remainingPermits) =>
new()
{
IsAllowed = true,
CurrentCount = currentCount,
RemainingPermits = remainingPermits,
RetryAfter = null
};
/// <summary>
/// Creates a result indicating the request was denied.
/// </summary>
public static RateLimitResult Denied(int currentCount, TimeSpan retryAfter) =>
new()
{
IsAllowed = false,
CurrentCount = currentCount,
RemainingPermits = 0,
RetryAfter = retryAfter
};
}
/// <summary>
/// Current status of a rate limit key.
/// </summary>
public readonly struct RateLimitStatus
{
/// <summary>
/// Gets the current count of permits used in the window.
/// </summary>
public int CurrentCount { get; init; }
/// <summary>
/// Gets the number of remaining permits in the window.
/// </summary>
public int RemainingPermits { get; init; }
/// <summary>
/// Gets the time remaining until the window resets.
/// </summary>
public TimeSpan WindowRemaining { get; init; }
/// <summary>
/// Gets whether the key exists (has any usage).
/// </summary>
public bool Exists { get; init; }
/// <summary>
/// Creates a status for an existing key.
/// </summary>
public static RateLimitStatus WithUsage(int currentCount, int remainingPermits, TimeSpan windowRemaining) =>
new()
{
CurrentCount = currentCount,
RemainingPermits = remainingPermits,
WindowRemaining = windowRemaining,
Exists = true
};
/// <summary>
/// Creates a status for a key with no usage.
/// </summary>
public static RateLimitStatus Empty(int maxPermits) =>
new()
{
CurrentCount = 0,
RemainingPermits = maxPermits,
WindowRemaining = TimeSpan.Zero,
Exists = false
};
}

View File

@@ -0,0 +1,148 @@
namespace StellaOps.Messaging;
/// <summary>
/// Result of a token issuance operation.
/// </summary>
public readonly struct TokenIssueResult
{
/// <summary>
/// Gets whether the token was issued successfully.
/// </summary>
public bool Success { get; init; }
/// <summary>
/// Gets the generated token value.
/// </summary>
public string Token { get; init; }
/// <summary>
/// Gets when the token expires.
/// </summary>
public DateTimeOffset ExpiresAt { get; init; }
/// <summary>
/// Gets the error message if issuance failed.
/// </summary>
public string? Error { get; init; }
/// <summary>
/// Creates a successful issuance result.
/// </summary>
public static TokenIssueResult Succeeded(string token, DateTimeOffset expiresAt) =>
new()
{
Success = true,
Token = token,
ExpiresAt = expiresAt
};
/// <summary>
/// Creates a failed issuance result.
/// </summary>
public static TokenIssueResult Failed(string error) =>
new()
{
Success = false,
Token = string.Empty,
Error = error
};
}
/// <summary>
/// Result of a token consumption attempt.
/// </summary>
/// <typeparam name="TPayload">The type of metadata payload stored with the token.</typeparam>
public readonly struct TokenConsumeResult<TPayload>
{
/// <summary>
/// Gets the status of the consumption attempt.
/// </summary>
public TokenConsumeStatus Status { get; init; }
/// <summary>
/// Gets the payload associated with the token (when successful).
/// </summary>
public TPayload? Payload { get; init; }
/// <summary>
/// Gets when the token was issued (when available).
/// </summary>
public DateTimeOffset? IssuedAt { get; init; }
/// <summary>
/// Gets when the token expires/expired (when available).
/// </summary>
public DateTimeOffset? ExpiresAt { get; init; }
/// <summary>
/// Gets whether the consumption was successful.
/// </summary>
public bool IsSuccess => Status == TokenConsumeStatus.Success;
/// <summary>
/// Creates a successful consumption result.
/// </summary>
public static TokenConsumeResult<TPayload> Success(TPayload payload, DateTimeOffset issuedAt, DateTimeOffset expiresAt) =>
new()
{
Status = TokenConsumeStatus.Success,
Payload = payload,
IssuedAt = issuedAt,
ExpiresAt = expiresAt
};
/// <summary>
/// Creates a not found result.
/// </summary>
public static TokenConsumeResult<TPayload> NotFound() =>
new()
{
Status = TokenConsumeStatus.NotFound
};
/// <summary>
/// Creates an expired result.
/// </summary>
public static TokenConsumeResult<TPayload> Expired(DateTimeOffset issuedAt, DateTimeOffset expiresAt) =>
new()
{
Status = TokenConsumeStatus.Expired,
IssuedAt = issuedAt,
ExpiresAt = expiresAt
};
/// <summary>
/// Creates a mismatch result (token exists but value doesn't match).
/// </summary>
public static TokenConsumeResult<TPayload> Mismatch() =>
new()
{
Status = TokenConsumeStatus.Mismatch
};
}
/// <summary>
/// Status of a token consumption attempt.
/// </summary>
public enum TokenConsumeStatus
{
/// <summary>
/// Token was consumed successfully.
/// </summary>
Success,
/// <summary>
/// Token was not found (doesn't exist or already consumed).
/// </summary>
NotFound,
/// <summary>
/// Token has expired.
/// </summary>
Expired,
/// <summary>
/// Token exists but the provided value doesn't match.
/// </summary>
Mismatch
}