liminal-protocol 0.14.1

Shared participant-lifecycle protocol types for liminal
Documentation
//! The exact observer-progress projection a committed source emits, moved out of
//! `observer_recovery.rs` unchanged so that file stays within the 500-line bound.

use crate::wire::{ConversationId, DeliverySeq};

/// Move-only exact observer-progress projection emitted by a committed source.
///
/// Participant acknowledgement and binding-ending transitions own the progress
/// value. Consuming code can persist and publish this projection but cannot
/// construct one from a guessed maximum or record-delivery observation.
///
/// ```compile_fail
/// use liminal_protocol::lifecycle::ObserverProgressProjection;
///
/// fn require_clone<T: Clone>() {}
/// require_clone::<ObserverProgressProjection>();
/// ```
#[derive(Debug, Eq)]
pub struct ObserverProgressProjection {
    conversation_id: ConversationId,
    new_observer_progress: DeliverySeq,
    source: Option<crate::lifecycle::checkpoint_codec::ObserverProjectionSource>,
}

impl PartialEq for ObserverProgressProjection {
    fn eq(&self, other: &Self) -> bool {
        self.conversation_id == other.conversation_id
            && self.new_observer_progress == other.new_observer_progress
    }
}

impl ObserverProgressProjection {
    #[cfg(any(test, feature = "test-support"))]
    pub(in crate::lifecycle) const fn new(
        conversation_id: ConversationId,
        new_observer_progress: DeliverySeq,
    ) -> Self {
        Self {
            conversation_id,
            new_observer_progress,
            source: None,
        }
    }

    pub(in crate::lifecycle) const fn sourced(
        conversation_id: ConversationId,
        new_observer_progress: DeliverySeq,
        source: crate::lifecycle::checkpoint_codec::ObserverProjectionSource,
    ) -> Self {
        Self::with_checkpoint_source(conversation_id, new_observer_progress, Some(source))
    }

    pub(in crate::lifecycle) const fn with_checkpoint_source(
        conversation_id: ConversationId,
        new_observer_progress: DeliverySeq,
        source: Option<crate::lifecycle::checkpoint_codec::ObserverProjectionSource>,
    ) -> Self {
        Self {
            conversation_id,
            new_observer_progress,
            source,
        }
    }

    /// Serializes the original native producer retained by this projection.
    ///
    /// # Errors
    /// Refuses a projection without native producer provenance.
    #[doc(hidden)]
    pub fn encode_checkpoint_source(
        &self,
    ) -> Result<alloc::vec::Vec<u8>, crate::lifecycle::OwnerCheckpointRestoreError> {
        crate::lifecycle::checkpoint_codec::encode_observer_source(
            self.conversation_id,
            self.new_observer_progress,
            self.source
                .ok_or(crate::lifecycle::OwnerCheckpointRestoreError::Field(
                    "observer_projection.native_source",
                ))?,
        )
    }

    /// Returns the participant named by the original native projection source.
    #[doc(hidden)]
    #[must_use]
    pub const fn checkpoint_source_participant_id(&self) -> Option<crate::wire::ParticipantId> {
        match self.source {
            Some(source) => Some(source.participant_id()),
            None => None,
        }
    }

    /// Returns the binding epoch retained by the native source, when it ended one.
    #[doc(hidden)]
    #[must_use]
    pub const fn checkpoint_source_binding_epoch(&self) -> Option<crate::wire::BindingEpoch> {
        match self.source {
            Some(source) => source.binding_epoch(),
            None => None,
        }
    }

    /// Returns the native producer class used to associate saved witness metadata.
    #[doc(hidden)]
    #[must_use]
    pub const fn checkpoint_source_kind(
        &self,
    ) -> Option<crate::lifecycle::checkpoint_codec::ObserverProjectionCheckpointKind> {
        match self.source {
            Some(source) => Some(source.kind()),
            None => None,
        }
    }

    /// Builds a projection no transition produced, for a test that must
    /// present a source a real transition never would.
    ///
    /// Absent from production builds (`test-support` / `cfg(test)` only).
    #[cfg(any(test, feature = "test-support"))]
    #[must_use]
    pub const fn for_test(
        conversation_id: ConversationId,
        new_observer_progress: DeliverySeq,
    ) -> Self {
        Self::new(conversation_id, new_observer_progress)
    }

    /// Returns the conversation whose hard observer progress may advance.
    #[must_use]
    pub const fn conversation_id(&self) -> ConversationId {
        self.conversation_id
    }

    /// Returns the exact protocol-produced hard observer progress.
    #[must_use]
    pub const fn new_observer_progress(&self) -> DeliverySeq {
        self.new_observer_progress
    }
}

impl ObserverProgressProjection {
    /// Borrows the source conversation and exact hard progress into canonical bytes.
    #[must_use]
    pub fn encode_checkpoint(&self) -> [u8; 16] {
        let mut body = [0; 16];
        body[..8].copy_from_slice(&self.conversation_id.to_be_bytes());
        body[8..].copy_from_slice(&self.new_observer_progress.to_be_bytes());
        body
    }

    /// Reassociates saved bytes with their protocol-produced committed source.
    ///
    /// The validated source moves into the result; this does not mint a projection.
    ///
    /// # Errors
    ///
    /// Refuses any framing, conversation, or exact progress mismatch.
    pub fn restore_checkpoint(
        body: &[u8],
        source: Self,
    ) -> Result<Self, crate::lifecycle::OwnerCheckpointRestoreError> {
        if body != source.encode_checkpoint() {
            return Err(crate::lifecycle::OwnerCheckpointRestoreError::Field(
                "observer_projection.source",
            ));
        }
        Ok(source)
    }
}