reliar-inbox 0.1.0

Transactional inbox deduplication: InboxStore/InboxHandler contracts, claim/complete/fail/purge semantics (no storage or transport dependency).
Documentation
//! What [`crate::InboxStore::claim`] found, what [`crate::InboxStore::process`] hands back, and
//! what [`crate::InboxStore::fail`] recorded.

use time::OffsetDateTime;

use crate::record_id::InboxRecordId;

/// The authoritative answer to "has this consumer already handled this message?", taken
/// **inside the caller's transaction**. A pre-check before the transaction is only ever an
/// optimisation; this one subsumes it, and there is deliberately no pre-check method.
///
/// ```
/// use reliar_inbox::InboxClaim;
///
/// let claim = InboxClaim::Claimed { attempt: 1 };
/// assert_eq!(claim, InboxClaim::Claimed { attempt: 1 });
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum InboxClaim {
    /// This transaction now owns the message. Run the handler, call
    /// [`crate::InboxStore::complete`], commit, **then** ack.
    Claimed {
        /// The ordinal of this attempt, 1-based — 1 on a first delivery, `n + 1` when `n`
        /// earlier attempts were recorded by [`crate::InboxStore::fail`].
        ///
        /// A **lower bound**: an attempt whose process died between its rollback and its `fail`
        /// is never counted (ADR 0042 §4). [`crate::InboxStore::fail`] *does* decide on the
        /// count — it sets `dead_at` once `attempts` reaches the provider's `max_attempts`
        /// (ADR 0042 A.2.4). Because the count is a lower bound, `max_attempts` bounds
        /// **recorded** failures only. Set it at or below the consumer's `max_deliver`, or the
        /// broker gives up first and [`Self::Dead`] is never reached.
        attempt: u32,
    },

    /// A previous delivery completed and committed. Nothing was written; roll the transaction
    /// back and ack. This is the hot path of a redelivery storm and performs **no write**.
    AlreadyCompleted {
        /// Database time at which the completing transaction ran.
        completed_at: OffsetDateTime,
    },

    /// Another transaction is handling this message right now. Nothing was written and the
    /// caller's transaction is **still usable**; roll it back and `nak` with a delay so the
    /// broker redelivers after the other attempt has settled.
    ///
    /// Reported immediately rather than waited out (ADR 0042 §3). The guard is a hashed
    /// transaction advisory lock, so a collision between two *concurrently claimed* keys can
    /// report this spuriously — a redelivery, never a lost or doubled effect.
    InProgress,

    /// The message failed `max_attempts` recorded attempts and the row is dead (ADR 0042
    /// A.2.4). Nothing was written; roll the transaction back and **`term`** — do not run the
    /// handler, which is the whole reason this answer exists.
    ///
    /// `term` rather than `ack`: both stop redelivery, but `ack` reports the message as
    /// successfully processed in every consumer metric and advisory an operator watches, while
    /// `term` emits `$JS.EVENT.ADVISORY.CONSUMER.MSG_TERMINATED`. A transport with no `term`
    /// acks and knows what it is substituting.
    ///
    /// The row is the operator's evidence: quote `id` to [`crate::InboxDeadLetters`]. Only
    /// [`crate::InboxDeadLetters::retry_dead`] leaves this state — and it does **not** cause a
    /// redelivery.
    Dead {
        /// The row's operator handle.
        id: InboxRecordId,

        /// Recorded failures at the moment it died — at least `max_attempts`.
        attempts: u32,

        /// Database time at which the bound was reached.
        dead_at: OffsetDateTime,
    },
}

/// [`crate::InboxStore::process`]'s result, and the caller's instruction sheet.
///
/// ```
/// use reliar_inbox::InboxOutcome;
///
/// let outcome: InboxOutcome<()> = InboxOutcome::Processed(());
/// assert_eq!(outcome, InboxOutcome::Processed(()));
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum InboxOutcome<T> {
    /// Handler ran, inbox row marked complete. **Commit, then ack** — in that order, always.
    Processed(T),

    /// Roll back and ack.
    AlreadyCompleted {
        /// Database time at which the completing transaction ran.
        completed_at: OffsetDateTime,
    },

    /// Roll back and `nak` with a delay.
    InProgress,

    /// The handler is not run: the row is dead. Roll back and `term` (see [`InboxClaim::Dead`]).
    Dead {
        /// The row's operator handle.
        id: InboxRecordId,

        /// Recorded failures at the moment it died.
        attempts: u32,

        /// Database time at which the bound was reached.
        dead_at: OffsetDateTime,
    },
}

/// What [`crate::InboxStore::fail`] recorded — and therefore which broker call the caller makes
/// next. Three variants because there are exactly three distinct actions (ADR 0042 A.2.4).
///
/// ```
/// use reliar_inbox::InboxFailure;
///
/// let outcome = InboxFailure::Recorded { attempts: 1 };
/// assert_eq!(outcome, InboxFailure::Recorded { attempts: 1 });
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum InboxFailure {
    /// The attempt was counted and the message may be redelivered. **`nak`** with a delay.
    Recorded {
        /// Recorded failures after this one.
        attempts: u32,
    },

    /// `attempts` reached the provider's `max_attempts`; `dead_at` is now set. **`term`** —
    /// redelivering would only produce [`InboxClaim::Dead`] again.
    Dead {
        /// The row's operator handle.
        id: InboxRecordId,

        /// Recorded failures at the moment it died.
        attempts: u32,

        /// Database time at which the bound was reached.
        dead_at: OffsetDateTime,
    },

    /// Another attempt completed this message first, so the completed-row guard left it
    /// untouched and nothing was recorded. **`ack`** — the work is done.
    ///
    /// Free information: the guard already made the statement affect zero rows, and `RETURNING`
    /// says which branch ran.
    AlreadyCompleted,
}