liminal-protocol 0.14.0

Shared participant-lifecycle protocol types for liminal
Documentation
//! Retirement of one closed conversation from the observer-recovery aggregate
//! (DUR-027 R2).
//!
//! A retired conversation's progress row is removed, so the aggregate holds
//! live rows only and its snapshot does not grow with finished conversations.
//! The aggregate keeps no retired set: what keeps a retired id from coming back
//! is the server's tombstone, and an advance or arm presenting it afterwards is
//! answered `ConversationUnknown`, as for any untracked conversation.

use super::ObserverRecoveryAggregate;
use crate::wire::{ConversationId, ObserverEpoch};

/// Failure while retiring a conversation from the aggregate.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ObserverProgressRetireError {
    /// The conversation has no authoritative progress row.
    ConversationUnknown {
        /// Unknown conversation.
        conversation_id: ConversationId,
    },
    /// An observer arm is installed on the conversation: an observer is still
    /// waiting on it, and retiring would answer it by deleting what it waits on.
    Armed {
        /// Armed conversation.
        conversation_id: ConversationId,
        /// Epoch of the installed arm.
        epoch: ObserverEpoch,
    },
}

/// Total transactional retirement decision against the owned aggregate.
#[derive(Debug, PartialEq, Eq)]
pub enum ObserverProgressRetireDecision {
    /// The retirement was refused; the aggregate is unchanged.
    Refuse {
        /// Unchanged aggregate returned to its owner.
        aggregate: ObserverRecoveryAggregate,
        /// Exact refusal.
        error: ObserverProgressRetireError,
    },
    /// The retirement validated; the progress row goes when it commits.
    Commit(ObserverProgressRetireTransaction),
}

/// Ownership barrier between a validated retirement and its durable append.
///
/// The aggregate is unreachable while the transaction is pending, so no arm
/// can be installed on the conversation between the check and the removal.
/// Consuming [`Self::commit`] removes the progress row after the caller has
/// confirmed the durable retire row; consuming [`Self::abort`] returns the
/// aggregate unchanged.
#[derive(Debug, PartialEq, Eq)]
pub struct ObserverProgressRetireTransaction {
    aggregate: ObserverRecoveryAggregate,
    conversation_id: ConversationId,
}

impl ObserverProgressRetireTransaction {
    /// Returns the conversation whose retire row the durable append writes.
    #[must_use]
    pub const fn conversation_id(&self) -> ConversationId {
        self.conversation_id
    }

    /// Removes the conversation's progress row after a confirmed durable append.
    #[must_use]
    pub fn commit(mut self) -> ObserverRecoveryAggregate {
        self.aggregate.progress.remove(&self.conversation_id);
        self.aggregate
    }

    /// Cancels a failed durable append; the row stays.
    #[must_use]
    pub fn abort(self) -> ObserverRecoveryAggregate {
        self.aggregate
    }
}

impl ObserverRecoveryAggregate {
    /// Consumes the aggregate into one retirement transaction.
    ///
    /// An untracked conversation refuses `ConversationUnknown` and an armed one
    /// refuses `Armed`, each with the aggregate unchanged. Nothing mutates until
    /// [`ObserverProgressRetireTransaction::commit`].
    #[must_use]
    pub fn decide_retire(self, conversation_id: ConversationId) -> ObserverProgressRetireDecision {
        if !self.progress.contains_key(&conversation_id) {
            return ObserverProgressRetireDecision::Refuse {
                aggregate: self,
                error: ObserverProgressRetireError::ConversationUnknown { conversation_id },
            };
        }
        if let Some(&epoch) = self.armed.get(&conversation_id) {
            return ObserverProgressRetireDecision::Refuse {
                aggregate: self,
                error: ObserverProgressRetireError::Armed {
                    conversation_id,
                    epoch,
                },
            };
        }
        ObserverProgressRetireDecision::Commit(ObserverProgressRetireTransaction {
            aggregate: self,
            conversation_id,
        })
    }
}