namespace w4c_workflows.Services.Messaging; /// A single entry pulled off a Redis stream / (later) RabbitMQ queue. public sealed record StreamMessage(string Id, IReadOnlyDictionary Fields); /// /// Default event stream for handler-mode subscriptions when the YAML /// trigger does not name an explicit stream. /// public static class Streams { public const string DefaultEvents = "wf:{tenant}:events"; /// Worker → control-plane result events (task.result). public const string Results = "wf:{tenant}:results"; } /// /// Job dispatch for task.run messages. The v1 implementation is Redis /// Streams with consumer groups; a RabbitMQ transport can be swapped in behind /// this interface without touching the engine. /// public interface IJobQueue { /// Ensures the tenant's jobs consumer group exists. Task EnsureGroupAsync(string tenantId, CancellationToken ct); /// Enqueues a job; returns the stream entry id. Task EnqueueAsync(string tenantId, IReadOnlyDictionary fields, CancellationToken ct); /// Reads new (undelivered) jobs for a consumer. Task> ReadGroupAsync(string tenantId, string consumer, int count, CancellationToken ct); /// Claims jobs pending for longer than (redelivery). Task> ClaimPendingAsync(string tenantId, string consumer, TimeSpan minIdle, int count, CancellationToken ct); Task AckAsync(string tenantId, string messageId, CancellationToken ct); /// Copies a failed job to the DLQ and acks it out of the group. Task DeadLetterAsync(string tenantId, string messageId, IReadOnlyDictionary fields, string reason, CancellationToken ct); /// /// Writes a dead-letter entry without a source job to ack. Used by the /// control-plane saga path: a task that exhausted retries (or a failed /// compensation) has no live jobs-stream entry left to acknowledge. /// Task PublishDeadLetterAsync(string tenantId, IReadOnlyDictionary fields, string reason, CancellationToken ct); } /// /// Event publish/subscribe (Redis Streams now, RabbitMQ later). A /// handler-mode workflow subscribes to a named stream via /// trigger.stream; the trigger engine consumes it through a consumer /// group and runs the downstream nextId chain per event. /// public interface IEventBus { Task PublishAsync(string tenantId, string stream, IReadOnlyDictionary fields, CancellationToken ct); Task EnsureGroupAsync(string tenantId, string stream, CancellationToken ct); Task> ReadGroupAsync(string tenantId, string stream, string consumer, int count, CancellationToken ct); Task> ClaimPendingAsync(string tenantId, string stream, string consumer, TimeSpan minIdle, int count, CancellationToken ct); Task AckAsync(string tenantId, string stream, string messageId, CancellationToken ct); }