liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
//! Exhaustive route classifications for the existing wire surfaces.

use super::HotAction;
use liminal::protocol::FrameType;
use liminal_protocol::wire::{ClientDiscriminant, PARTICIPANT_FRAME_TYPE};
use serde::{Deserialize, Serialize};

/// Where the exact placed subject must be resolved before deciding.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SubjectSource {
    /// Subject carried by this frame and resolved through admitted mapping.
    FrameSubject,
    /// Subject retained by this connection's subscription.
    SubscriptionBinding,
    /// Subject retained by this connection's conversation.
    ConversationBinding,
    /// Every explicitly served subject in the admitted worker registration.
    WorkerBinding,
    /// Subject retained by the recipient-bound push correlation.
    PushBinding,
    /// Conversation-to-subject mapping, never a guessed numeric subject.
    ParticipantBinding,
    /// The link the frame offers, under the network, with the end it names.
    FrameLink,
    /// The link open on this connection's stream, and the frame's subject.
    LinkBinding,
}

/// Permission policy; control and ignored inbound frames create no data effect.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "class", rename_all = "snake_case")]
pub enum RouteClassification {
    /// One offline decision before the operation's effect.
    Hot {
        /// Required subject permission.
        action: HotAction,
        /// Existing authoritative binding used to resolve the subject.
        source: SubjectSource,
    },
    /// Release an already-owned resource without creating new reach.
    Cleanup {
        /// Permission associated with the released resource.
        action: HotAction,
        /// Binding whose ownership must still be checked.
        source: SubjectSource,
    },
    /// One live decision before the operation's effect, never the hot seam:
    /// the frame's offered end names `link_export` or `link_import` on the
    /// link (ACCESS-002 R1).
    Deliberate {
        /// Binding used to resolve the link and the end.
        source: SubjectSource,
    },
    /// A message crossing a link: `link_export` held by the exporting
    /// machine and `link_import` by the importing one on the opened link
    /// resource, both decided offline from the two passes (ACCESS-002 R1).
    Crossing {
        /// Binding used to resolve the link and the subject.
        source: SubjectSource,
    },
    /// Credential admission; no subject operation is authorized by this result.
    Admission,
    /// Liveness or connection teardown with no new subject effect.
    Control,
    /// Inbound server-originated frame, ignored without dispatch.
    ServerOnly,
    /// Decode the participant subrecord and classify it independently.
    ParticipantEnvelope,
    /// Unknown inbound frame; no data operation can dispatch.
    Unknown,
}

const fn hot(action: HotAction, source: SubjectSource) -> RouteClassification {
    RouteClassification::Hot { action, source }
}
const fn cleanup(action: HotAction, source: SubjectSource) -> RouteClassification {
    RouteClassification::Cleanup { action, source }
}

/// Classifies every core frame category, including the participant extension.
///
/// This match has no catch-all over known variants; a new wire route requires
/// an explicit policy before the classifier can compile.
#[must_use]
pub const fn classify_wire_route(frame: FrameType) -> RouteClassification {
    use SubjectSource as Source;
    match frame {
        FrameType::Connect => RouteClassification::Admission,
        FrameType::Disconnect | FrameType::Ping => RouteClassification::Control,
        FrameType::Publish => hot(HotAction::Publish, Source::FrameSubject),
        FrameType::Subscribe => hot(HotAction::Subscribe, Source::FrameSubject),
        FrameType::Unsubscribe => cleanup(HotAction::Subscribe, Source::SubscriptionBinding),
        FrameType::ConversationOpen => hot(HotAction::Call, Source::FrameSubject),
        FrameType::ConversationMessage => hot(HotAction::Call, Source::ConversationBinding),
        FrameType::ConversationClose => cleanup(HotAction::Call, Source::ConversationBinding),
        FrameType::Accept | FrameType::Defer | FrameType::Reject => {
            hot(HotAction::Subscribe, Source::SubscriptionBinding)
        }
        FrameType::PushReply => hot(HotAction::Serve, Source::PushBinding),
        FrameType::WorkerRegister => hot(HotAction::Serve, Source::WorkerBinding),
        FrameType::LinkOpen => RouteClassification::Deliberate {
            source: Source::FrameLink,
        },
        FrameType::LinkMessage => RouteClassification::Crossing {
            source: Source::LinkBinding,
        },
        FrameType::ConnectAck
        | FrameType::ConnectError
        | FrameType::SubscribeAck
        | FrameType::SubscribeError
        | FrameType::PublishAck
        | FrameType::PublishError
        | FrameType::ConversationError
        | FrameType::Pong
        | FrameType::Push
        | FrameType::WorkerRegisterAck
        | FrameType::LinkOpenAck
        | FrameType::DisconnectReason
        | FrameType::Deliver => RouteClassification::ServerOnly,
        FrameType::Unknown(tag) => {
            if tag == PARTICIPANT_FRAME_TYPE {
                RouteClassification::ParticipantEnvelope
            } else {
                RouteClassification::Unknown
            }
        }
    }
}

/// Classifies every decoded client participant subrecord.
///
/// A recovery batch must decide every mapped conversation before any effect.
/// Cleanup still requires the existing participant/connection ownership proof.
#[must_use]
pub const fn classify_participant_route(route: ClientDiscriminant) -> RouteClassification {
    use SubjectSource::ParticipantBinding as Source;
    match route {
        ClientDiscriminant::EnrollmentRequest
        | ClientDiscriminant::CredentialAttachRequest
        | ClientDiscriminant::ParticipantAck
        | ClientDiscriminant::MarkerAck
        | ClientDiscriminant::ObserverRecoveryHandshake
        | ClientDiscriminant::SettlementQuery => hot(HotAction::Subscribe, Source),
        ClientDiscriminant::RecordAdmission => hot(HotAction::Publish, Source),
        ClientDiscriminant::DetachRequest | ClientDiscriminant::LeaveRequest => {
            cleanup(HotAction::Subscribe, Source)
        }
    }
}