Skip to main content

tea_session/
error.rs

1use serde::{Deserialize, Serialize};
2use tea_protocol::{RecordId, SessionId, SessionSequence};
3use thiserror::Error;
4
5/// Stable storage failure classification shared by session-store adapters.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum SessionStoreErrorCode {
9    /// The requested session does not exist.
10    SessionNotFound,
11    /// Creation was requested for an existing session.
12    SessionAlreadyExists,
13    /// A writer supplied a stale or otherwise incorrect expected sequence.
14    SequenceConflict,
15    /// A durable record is malformed for the session state machine.
16    InvalidRecord,
17    /// A durable record references an unknown or incompatible entity.
18    InvalidReference,
19    /// The record or archive schema version is unsupported.
20    UnsupportedSchemaVersion,
21    /// Stored source facts violate append-only log invariants.
22    CorruptionDetected,
23    /// An atomic transaction could not be committed.
24    TransactionFailed,
25    /// The storage adapter is unavailable.
26    StorageUnavailable,
27}
28
29/// Deterministic failure while reducing durable records.
30#[derive(Debug, Clone, PartialEq, Eq, Error)]
31pub enum SessionReplayError {
32    /// Replay requires at least one creation record.
33    #[error("session log is empty")]
34    EmptyLog,
35    /// The first record is not the sole session creation record.
36    #[error("session creation record is missing or duplicated")]
37    InvalidCreation,
38    /// One envelope belongs to another session.
39    #[error("record belongs to session {actual}, expected {expected}")]
40    SessionMismatch {
41        /// Expected session identity.
42        expected: SessionId,
43        /// Actual envelope session identity.
44        actual: SessionId,
45    },
46    /// Authoritative sequence is not contiguous.
47    #[error("record sequence is {actual}, expected {expected}")]
48    SequenceMismatch {
49        /// Required next sequence.
50        expected: SessionSequence,
51        /// Actual sequence.
52        actual: SessionSequence,
53    },
54    /// Sequence cannot advance beyond its integer representation.
55    #[error("session sequence overflow")]
56    SequenceOverflow,
57    /// Record identity was already present in the log.
58    #[error("duplicate record ID: {record_id}")]
59    DuplicateRecord {
60        /// Reused record identity.
61        record_id: RecordId,
62    },
63    /// A globally stable entity identity was reused.
64    #[error("duplicate session entity: {entity}")]
65    DuplicateEntity {
66        /// Bounded technical entity category.
67        entity: &'static str,
68    },
69    /// A record references an entity that is absent or in the wrong state.
70    #[error("invalid session reference: {reference}")]
71    InvalidReference {
72        /// Bounded technical reference category.
73        reference: &'static str,
74    },
75    /// A known transition is invalid for the current state.
76    #[error("invalid session transition: {transition}")]
77    InvalidTransition {
78        /// Bounded technical transition category.
79        transition: &'static str,
80    },
81}
82
83impl SessionReplayError {
84    /// Maps replay failures to a stable storage-facing classification.
85    #[must_use]
86    pub const fn store_code(&self) -> SessionStoreErrorCode {
87        match self {
88            Self::InvalidReference { .. } => SessionStoreErrorCode::InvalidReference,
89            Self::SequenceMismatch { .. }
90            | Self::SequenceOverflow
91            | Self::DuplicateRecord { .. }
92            | Self::DuplicateEntity { .. }
93            | Self::SessionMismatch { .. }
94            | Self::EmptyLog
95            | Self::InvalidCreation => SessionStoreErrorCode::CorruptionDetected,
96            Self::InvalidTransition { .. } => SessionStoreErrorCode::InvalidRecord,
97        }
98    }
99}
100
101/// Stable session repository failure with an English technical diagnostic.
102#[derive(Debug, Clone, PartialEq, Eq, Error)]
103#[error("{code:?}: {message}")]
104pub struct SessionStoreError {
105    code: SessionStoreErrorCode,
106    message: String,
107}
108
109impl SessionStoreError {
110    /// Creates a bounded storage error.
111    #[must_use]
112    pub fn new(code: SessionStoreErrorCode, message: impl Into<String>) -> Self {
113        let mut message = message.into();
114        if message.len() > 4096 {
115            let boundary = message
116                .char_indices()
117                .map(|(index, _)| index)
118                .take_while(|index| *index <= 4096)
119                .last()
120                .unwrap_or(0);
121            message.truncate(boundary);
122        }
123        Self { code, message }
124    }
125
126    /// Returns the stable machine-readable classification.
127    #[must_use]
128    pub const fn code(&self) -> SessionStoreErrorCode {
129        self.code
130    }
131
132    /// Returns the bounded English technical diagnostic.
133    #[must_use]
134    pub fn message(&self) -> &str {
135        &self.message
136    }
137}
138
139impl From<SessionReplayError> for SessionStoreError {
140    fn from(error: SessionReplayError) -> Self {
141        Self::new(error.store_code(), error.to_string())
142    }
143}