Skip to main content

tirea_contract/storage/
types.rs

1use crate::thread::Thread;
2use crate::thread::Version;
3use crate::Message;
4use crate::Visibility;
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8/// Sort order for paginated queries.
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum SortOrder {
12    #[default]
13    Asc,
14    Desc,
15}
16
17/// Cursor-based pagination parameters for messages.
18#[derive(Debug, Clone)]
19pub struct MessageQuery {
20    /// Return messages with cursor strictly greater than this value.
21    pub after: Option<i64>,
22    /// Return messages with cursor strictly less than this value.
23    pub before: Option<i64>,
24    /// Maximum number of messages to return (clamped to 1..=200).
25    pub limit: usize,
26    /// Sort order.
27    pub order: SortOrder,
28    /// Filter by message visibility. `None` means return all messages.
29    pub visibility: Option<Visibility>,
30    /// Filter by run ID. `None` means return all runs.
31    pub run_id: Option<String>,
32}
33
34impl Default for MessageQuery {
35    fn default() -> Self {
36        Self {
37            after: None,
38            before: None,
39            limit: 50,
40            order: SortOrder::Asc,
41            visibility: Some(Visibility::All),
42            run_id: None,
43        }
44    }
45}
46
47/// A message paired with its storage-assigned cursor.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct MessageWithCursor {
50    pub cursor: i64,
51    #[serde(flatten)]
52    pub message: Message,
53}
54
55/// Paginated message response.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct MessagePage {
58    pub messages: Vec<MessageWithCursor>,
59    pub has_more: bool,
60    /// Cursor of the last item.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub next_cursor: Option<i64>,
63    /// Cursor of the first item.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub prev_cursor: Option<i64>,
66}
67
68/// Pagination query for Thread lists.
69#[derive(Debug, Clone)]
70pub struct ThreadListQuery {
71    /// Number of items to skip (0-based).
72    pub offset: usize,
73    /// Maximum number of items to return (clamped to 1..=200).
74    pub limit: usize,
75    /// Filter by resource_id (owner). `None` means no filtering.
76    pub resource_id: Option<String>,
77    /// Filter by parent_thread_id. `None` means no filtering.
78    pub parent_thread_id: Option<String>,
79}
80
81impl Default for ThreadListQuery {
82    fn default() -> Self {
83        Self {
84            offset: 0,
85            limit: 50,
86            resource_id: None,
87            parent_thread_id: None,
88        }
89    }
90}
91
92/// Paginated Thread list response.
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ThreadListPage {
95    pub items: Vec<String>,
96    pub total: usize,
97    pub has_more: bool,
98}
99
100/// Cursor-based in-memory pagination helper.
101pub fn paginate_in_memory(
102    messages: &[std::sync::Arc<Message>],
103    query: &MessageQuery,
104) -> MessagePage {
105    let limit = query.limit.clamp(1, 200);
106    let total = messages.len();
107    if total == 0 {
108        return MessagePage {
109            messages: Vec::new(),
110            has_more: false,
111            next_cursor: None,
112            prev_cursor: None,
113        };
114    }
115
116    let start = query.after.map(|c| (c + 1).max(0) as usize).unwrap_or(0);
117    let end = query
118        .before
119        .map(|c| (c.max(0) as usize).min(total))
120        .unwrap_or(total);
121
122    if start >= total || start >= end {
123        return MessagePage {
124            messages: Vec::new(),
125            has_more: false,
126            next_cursor: None,
127            prev_cursor: None,
128        };
129    }
130
131    let mut items: Vec<(i64, &std::sync::Arc<Message>)> = messages[start..end]
132        .iter()
133        .enumerate()
134        .filter(|(_, m)| match query.visibility {
135            Some(vis) => m.visibility == vis,
136            None => true,
137        })
138        .filter(|(_, m)| match &query.run_id {
139            Some(rid) => {
140                m.metadata.as_ref().and_then(|meta| meta.run_id.as_deref()) == Some(rid.as_str())
141            }
142            None => true,
143        })
144        .map(|(i, m)| ((start + i) as i64, m))
145        .collect();
146
147    if query.order == SortOrder::Desc {
148        items.reverse();
149    }
150
151    let has_more = items.len() > limit;
152    let limited: Vec<_> = items.into_iter().take(limit).collect();
153
154    MessagePage {
155        next_cursor: limited.last().map(|(c, _)| *c),
156        prev_cursor: limited.first().map(|(c, _)| *c),
157        messages: limited
158            .into_iter()
159            .map(|(c, m)| MessageWithCursor {
160                cursor: c,
161                message: (**m).clone(),
162            })
163            .collect(),
164        has_more,
165    }
166}
167
168/// Storage-level errors.
169#[derive(Debug, Error)]
170pub enum ThreadStoreError {
171    /// Thread not found.
172    #[error("Thread not found: {0}")]
173    NotFound(String),
174
175    /// IO error.
176    #[error("IO error: {0}")]
177    Io(#[from] std::io::Error),
178
179    /// Serialization error.
180    #[error("Serialization error: {0}")]
181    Serialization(String),
182
183    /// Invalid Thread id (path traversal, control chars, etc.).
184    #[error("Invalid thread id: {0}")]
185    InvalidId(String),
186
187    /// Thread already exists.
188    #[error("Thread already exists")]
189    AlreadyExists,
190
191    /// Optimistic concurrency check failed.
192    #[error("Version conflict: expected {expected}, actual {actual}")]
193    VersionConflict { expected: Version, actual: Version },
194}
195
196/// Version check policy for append operations.
197#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
198#[serde(rename_all = "snake_case")]
199pub enum VersionPrecondition {
200    /// Skip version check before commit.
201    #[default]
202    Any,
203    /// Require an exact version match before commit.
204    Exact(Version),
205}
206
207/// Commit acknowledgement returned after successful write.
208#[derive(Debug, Clone, Copy)]
209pub struct Committed {
210    pub version: Version,
211}
212
213/// Thread plus current storage version.
214#[derive(Debug, Clone)]
215pub struct ThreadHead {
216    pub thread: Thread,
217    pub version: Version,
218}