Skip to main content

meerkat_store/
error.rs

1//! Storage errors
2
3use meerkat_core::{SessionId, SessionStoreError};
4
5/// Backend-specific error type used internally by meerkat-store implementations.
6///
7/// External consumers should use [`SessionStoreError`] (from `meerkat-core`) for
8/// the `SessionStore` trait boundary. This type carries backend-specific variants
9/// (for example rusqlite) that the trait contract intentionally erases.
10#[derive(Debug, thiserror::Error)]
11pub enum StoreError {
12    #[error("IO error: {0}")]
13    Io(#[from] std::io::Error),
14
15    #[error("Serialization error: {0}")]
16    Serialization(#[from] serde_json::Error),
17
18    #[cfg(not(target_arch = "wasm32"))]
19    #[error("SQLite error: {0}")]
20    Sqlite(#[from] rusqlite::Error),
21
22    #[error("Session not found: {0}")]
23    NotFound(SessionId),
24
25    #[error("Session corrupted: {0}")]
26    Corrupted(SessionId),
27
28    #[cfg(not(target_arch = "wasm32"))]
29    #[error("Task join error: {0}")]
30    Join(#[from] tokio::task::JoinError),
31
32    #[error("Internal error: {0}")]
33    Internal(String),
34
35    #[cfg(not(target_arch = "wasm32"))]
36    #[error("timed out acquiring realm manifest lock for '{realm_id}'")]
37    RealmManifestLockTimeout { realm_id: String },
38
39    #[cfg(not(target_arch = "wasm32"))]
40    #[error(
41        "realm backend mismatch for '{realm_id}': requested '{requested}', existing '{existing}'"
42    )]
43    RealmBackendMismatch {
44        realm_id: String,
45        requested: String,
46        existing: String,
47    },
48
49    #[cfg(not(target_arch = "wasm32"))]
50    #[error("unsupported realm backend for '{realm_id}': '{backend}'")]
51    UnsupportedRealmBackend { realm_id: String, backend: String },
52
53    /// The requested realm id sanitizes to the same on-disk path as an
54    /// existing manifest that pins a *different* realm identity (e.g. the
55    /// raw slugs `a.b` and `a_b` both sanitize to the `a_b` directory).
56    /// Two distinct realm identities must never silently share one
57    /// manifest, so the path-aliased open is rejected fail-closed rather
58    /// than handing back the wrong realm's manifest.
59    #[cfg(not(target_arch = "wasm32"))]
60    #[error(
61        "realm identity mismatch: requested '{requested}' aliases existing manifest '{existing}'"
62    )]
63    RealmIdentityMismatch { requested: String, existing: String },
64
65    /// Persisted manifest carried a realm id that fails the typed
66    /// slug validator (wave-c C-12 sibling retype — the on-disk form
67    /// is free-string but the domain type is `RealmId` which enforces
68    /// the slug grammar). Reported when an on-disk manifest was
69    /// hand-edited to an unparseable realm slug.
70    #[cfg(not(target_arch = "wasm32"))]
71    #[error("invalid realm id slug in persisted manifest: '{0}'")]
72    InvalidRealmSlug(String),
73}
74
75impl StoreError {
76    /// Convert to the backend-agnostic [`SessionStoreError`] at the trait boundary.
77    pub fn into_session_store_error(self) -> SessionStoreError {
78        match self {
79            StoreError::Io(e) => SessionStoreError::Io(e),
80            StoreError::Serialization(e) => SessionStoreError::Serialization(e.to_string()),
81            StoreError::NotFound(id) => SessionStoreError::NotFound(id),
82            StoreError::Corrupted(id) => SessionStoreError::Corrupted(id),
83            other => SessionStoreError::Internal(other.to_string()),
84        }
85    }
86}
87
88/// Convert [`StoreError`] to [`SessionStoreError`] at the trait boundary.
89///
90/// Used as `.map_err(into_session_store_error)` in `SessionStore` trait impls.
91/// Only needed on native targets where the persistent store backends exist.
92#[cfg(not(target_arch = "wasm32"))]
93#[cfg(any(feature = "jsonl", feature = "sqlite"))]
94pub(crate) fn into_session_store_error(e: StoreError) -> SessionStoreError {
95    e.into_session_store_error()
96}